├── public ├── js │ ├── server │ │ ├── webClientTest.js │ │ ├── connect.js │ │ ├── integrationTests.js │ │ ├── runTests.js │ │ ├── inventoryTest.js │ │ ├── webClientTestData.js │ │ ├── SocketToAccountMap.js │ │ ├── robotTest.js │ │ ├── robotTestData.js │ │ └── customizeServer.js │ ├── config │ │ └── config.example.js │ ├── client │ │ ├── Game.mjs │ │ ├── cmd.js │ │ ├── WorldAndScenePoint.mjs │ │ ├── VoxelMap.mjs │ │ ├── CoordForm.mjs │ │ ├── CutawayForm.mjs │ │ ├── Robot.mjs │ │ ├── bannerMessages.mjs │ │ ├── WebClient.mjs │ │ └── InventoryRender.mjs │ ├── shared │ │ ├── recipeSearch.js │ │ ├── MapData.js │ │ ├── fromClientSchemas.js │ │ ├── InventoryData.js │ │ └── fromRobotSchemas.js │ └── lib │ │ └── PointerLockControls.js ├── favicon.ico ├── assets │ ├── tree.gif │ ├── placeholder_icon.ico │ ├── placeholder_icon.png │ ├── placeholder_icon.icns │ ├── cube.svg │ └── squaredcube.svg ├── lua │ └── oc │ │ ├── config.txt │ │ ├── setup.lua │ │ ├── login │ │ └── login.lua │ │ ├── downloadCode.lua │ │ ├── tcp.lua │ │ ├── adjacent.lua │ │ ├── sendScan.lua │ │ ├── scanDirection.lua │ │ ├── commandLoop.lua │ │ ├── trackOrientation.lua │ │ ├── trackPosition.lua │ │ ├── config.lua │ │ ├── moveAndScan.lua │ │ ├── commandMap.lua │ │ ├── doToArea.lua │ │ ├── interact.lua │ │ └── craft.lua └── css │ ├── accounts.css │ ├── style.css │ └── bootstrap-select.min.css ├── documentation ├── test-setup.md ├── web_install.txt ├── local_install.txt ├── standalone-install.md ├── creative-robot-install.md ├── server-install.md ├── faq.md ├── survival-robot-install.md ├── protocol.md └── tips.md ├── views ├── error.ejs ├── login.ejs └── index.ejs ├── .gitignore ├── jsconfig.json ├── release_checklist.md ├── LICENSE ├── package.json ├── bin └── www ├── readme.md ├── electronApp.js ├── routes └── routes.js ├── app.js ├── todo.md └── testplan.md /public/js/server/webClientTest.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunstad/roboserver/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/assets/tree.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunstad/roboserver/HEAD/public/assets/tree.gif -------------------------------------------------------------------------------- /public/lua/oc/config.txt: -------------------------------------------------------------------------------- 1 | {posX=0,serverIP="127.0.0.1",serverPort="8080",posY=0,tcpPort=3001,posZ=0} -------------------------------------------------------------------------------- /public/assets/placeholder_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunstad/roboserver/HEAD/public/assets/placeholder_icon.ico -------------------------------------------------------------------------------- /public/assets/placeholder_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunstad/roboserver/HEAD/public/assets/placeholder_icon.png -------------------------------------------------------------------------------- /public/assets/placeholder_icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dunstad/roboserver/HEAD/public/assets/placeholder_icon.icns -------------------------------------------------------------------------------- /public/js/server/connect.js: -------------------------------------------------------------------------------- 1 | testData = require('./robotTestData'); 2 | testClient = new (require('./TestClient'))(testData); 3 | testClient.connect(); -------------------------------------------------------------------------------- /public/css/accounts.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 5px; 3 | } 4 | 5 | .padded { 6 | margin: 5px; 7 | } 8 | 9 | .bigtext { 10 | font-size: 24px; 11 | } -------------------------------------------------------------------------------- /public/js/config/config.example.js: -------------------------------------------------------------------------------- 1 | var config = {}; 2 | 3 | config.webServerPort = '8080'; 4 | config.expressSessionSecret = "testSecret" 5 | 6 | module.exports = config; -------------------------------------------------------------------------------- /documentation/test-setup.md: -------------------------------------------------------------------------------- 1 | ### Test Setup 2 | * download chromedriver 3 | * if you've done ```npm install --dev``` already, the selenium-webdriver module should be available 4 | * run ```public/server/integrationTests.js``` -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

<%= message %>

8 |

<%= error %>

9 | 10 | 11 | -------------------------------------------------------------------------------- /documentation/web_install.txt: -------------------------------------------------------------------------------- 1 | mkdir /home/lib; 2 | set ROBOSERVER_CODE=https://raw.githubusercontent.com/dunstad/roboserver/master/public/lua/oc; 3 | wget $ROBOSERVER_CODE/setup.lua /home/lib/setup.lua; 4 | lua /home/lib/setup.lua; -------------------------------------------------------------------------------- /documentation/local_install.txt: -------------------------------------------------------------------------------- 1 | mkdir /home/lib; 2 | -- if you're not on localhost or you changed the port, modify those here 3 | set ROBOSERVER_CODE=http://localhost:8080/lua/oc; 4 | wget $ROBOSERVER_CODE/setup.lua /home/lib/setup.lua; 5 | lua /home/lib/setup.lua; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public/js/lib/three.js-master/* 2 | /public/js/config/config.js 3 | /recipes/* 4 | /node_modules/* 5 | /.cache/* 6 | /.mocha-puppeteer/* 7 | *.log 8 | *.db 9 | .DS_Store 10 | /lua/serialization.lua 11 | /lua/robot.lua 12 | /lua/internet.lua 13 | /release-builds/* -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" 4 | }, 5 | "include": [ 6 | "public/js/**/*", 7 | "public/js/lib/*", 8 | "public/js/lib/three.min.js" 9 | ], 10 | "typeAcquisition": { 11 | "include": [ 12 | "three", 13 | "socket.io-client" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /release_checklist.md: -------------------------------------------------------------------------------- 1 | ## release checklist 2 | * do everything in [testplan.md](testplan.md) 3 | * change the version number in [package.json](package.json) 4 | * change the version number in [creative-robot-install.md](creative-robot-install.md) and (survival-robot-install.md)[survival-robot-install.md] 5 | * push to master branch 6 | * make a develop branch 7 | * add a tag to the release commit -------------------------------------------------------------------------------- /public/lua/oc/setup.lua: -------------------------------------------------------------------------------- 1 | local path = '/home/lib'; 2 | local os = require('os'); 3 | local codeURL = os.getenv('ROBOSERVER_CODE'); 4 | local fileName = '/downloadCode.lua' 5 | os.execute('wget -f ' .. codeURL .. fileName .. ' ' .. path .. fileName); 6 | local dl = require("downloadCode"); 7 | dl.downloadAll(path); 8 | local config = require("config"); 9 | config.easy(config.path); 10 | require('commandLoop'); 11 | -------------------------------------------------------------------------------- /public/assets/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/js/server/integrationTests.js: -------------------------------------------------------------------------------- 1 | var webdriver = require('selenium-webdriver'); 2 | var browser = new webdriver.Builder().usingServer().withCapabilities({'browserName': 'chrome' }).build(); 3 | 4 | // this is just an example 5 | 6 | browser.get('http://en.wikipedia.org/wiki/Wiki'); 7 | browser.findElements(webdriver.By.css('[href^="/wiki/"]')).then(function(links){ 8 | console.log('Found', links.length, 'Wiki links.' ) 9 | browser.quit(); 10 | }); -------------------------------------------------------------------------------- /public/js/client/Game.mjs: -------------------------------------------------------------------------------- 1 | import {GUI} from '/js/client/GUI.mjs'; 2 | import {MapRender} from '/js/client/MapRender.mjs'; 3 | import {WebClient} from '/js/client/WebClient.mjs'; 4 | 5 | export class Game { 6 | 7 | /** 8 | * Used to connect the MapRender, GUI, and other things to each other. 9 | */ 10 | constructor() { 11 | 12 | this.mapRender = new MapRender(this); 13 | this.GUI = new GUI(this); 14 | this.webClient = new WebClient(this); 15 | 16 | } 17 | 18 | }; -------------------------------------------------------------------------------- /public/lua/oc/login/login.lua: -------------------------------------------------------------------------------- 1 | local debug = require("component").debug; 2 | local world = debug.getWorld(); 3 | 4 | function getBlock(x, y, z) 5 | return { 6 | ["id"] = world.getBlockId(x, y, z), 7 | ["metadata"] = world.getMetadata(x, y, z), 8 | ["nbt"] = world.getTileNBT(x, y, z) 9 | }; 10 | end 11 | 12 | function setBlock(x, y, z, block) 13 | world.setBlock(x, y, z, block["id"], 0); -- need to get numeric metadata into block["metadata"] 14 | world.setTileNBT(x, y, z, block["nbt"]); 15 | end -------------------------------------------------------------------------------- /public/assets/squaredcube.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /documentation/standalone-install.md: -------------------------------------------------------------------------------- 1 | ### Standalone 2 | 3 | You can download Roboserver for Windows, OS X, or Linux [here (TODO)](TODO). Unpack and run it when the download finishes. You'll also need to remove "127.0.0.0/8" from the blacklist in your OpenComputers configuration file, otherwise your robot will be unable to connect. 4 | 5 | Congratulations, you're halfway done! Now let's get a robot set up. 6 | 7 | If you just want to try the program out without any fuss, I recommend using a Creative robot. Read how [here](creative-robot-install.md). If you want to use this in a survival world, you'll want to follow the steps [here](survival-robot-install.md). -------------------------------------------------------------------------------- /documentation/creative-robot-install.md: -------------------------------------------------------------------------------- 1 | #### Creative 2 | 3 | Set down the Creatix robot, run ```install```, select OpenOS, and reboot it when the install completes. Make sure to put a Geolyzer in its upgrade slot (not its tool slot!). 4 | 5 | Now that your robot is running and OpenOS is installed, just paste [this](web_install.txt) into it. 6 | (If your Roboserver is running but you don't have internet access, you can use the [local install](local_install.txt) instead.) 7 | 8 | After answering a few questions about your robot, it will connect to the server you started in the previous step. Congratulations, you're done! Next check out [these tips](tips.md) on how to use the Roboserver. -------------------------------------------------------------------------------- /public/js/client/cmd.js: -------------------------------------------------------------------------------- 1 | // For easy use from the dev console 2 | 3 | var commandInput = document.getElementById("commandInput"); 4 | var runInTerminal = document.getElementById("runInTerminal"); 5 | var craftButton = document.getElementById("craftButton"); 6 | var craftSelect = document.getElementById("craftSelect"); 7 | 8 | function sendEnter(elem) { 9 | var e = new Event("keydown"); 10 | e.keyCode = 13; 11 | elem.dispatchEvent(e); 12 | } 13 | 14 | function send(command, asShell) { 15 | if (asShell) {runInTerminal.checked = true;} 16 | else {runInTerminal.checked = false;} 17 | commandInput.value = command; 18 | sendEnter(commandInput); 19 | } 20 | 21 | function craft(itemName) { 22 | $('.selectpicker').selectpicker('val', itemName); 23 | craftButton.click(); 24 | } -------------------------------------------------------------------------------- /public/js/server/runTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs a test suite. Used for unit testing. 3 | * @param {object} tests 4 | * @param {function} setup 5 | * @param {object} testData 6 | */ 7 | function runTests(tests, setup, testData) { 8 | 9 | passedTests = 0; 10 | failedTests = 0; 11 | 12 | for (let testName in tests) { 13 | 14 | try { 15 | tests[testName](setup(testData)); 16 | passedTests++; 17 | // console.log(testName, "passed"); 18 | // console.log(); 19 | } 20 | catch (e) { 21 | console.log(testName, "failed"); 22 | console.log(e); 23 | console.log(); 24 | failedTests++; 25 | } 26 | } 27 | 28 | console.log("passed tests:", passedTests); 29 | console.log("failed tests:", failedTests); 30 | console.log(); 31 | 32 | } 33 | 34 | module.exports = runTests; -------------------------------------------------------------------------------- /public/lua/oc/downloadCode.lua: -------------------------------------------------------------------------------- 1 | local os = require('os'); 2 | 3 | local url = os.getenv('ROBOSERVER_CODE') .. '/'; 4 | local filenames = { 5 | 'commandLoop.lua', 6 | 'commandMap.lua', 7 | 'json.lua', 8 | 'scanDirection.lua', 9 | 'sendScan.lua', 10 | 'tcp.lua', 11 | 'trackPosition.lua', 12 | 'trackOrientation.lua', 13 | 'moveAndScan.lua', 14 | 'adjacent.lua', 15 | 'doToArea.lua', 16 | 'interact.lua', 17 | 'craft.lua', 18 | 'config.lua', 19 | -- 'config.txt', -- overwriting the config when updating isn't nice 20 | }; 21 | 22 | local M = {}; 23 | 24 | function M.downloadAll(location) 25 | for index, name in pairs(filenames) do 26 | M.download(name, location); 27 | end 28 | end 29 | 30 | -- rapid reuse may result in receiving cached pages 31 | function M.download(name, location) 32 | os.execute('wget -f ' .. url .. name .. ' ' .. location .. '/' .. name); 33 | end 34 | 35 | return M; 36 | -------------------------------------------------------------------------------- /public/js/server/inventoryTest.js: -------------------------------------------------------------------------------- 1 | const testData = require('./robotTestData'); 2 | const validators = require('../shared/fromRobotSchemas.js').validators; 3 | const assert = require('assert'); 4 | const InventoryData = require('../shared/InventoryData'); 5 | const runTests = require('./runTests.js'); 6 | 7 | function setup(testData) { 8 | let inventory = new InventoryData(testData.internalInventory.meta); 9 | for (let slot of testData.internalInventory.slots) { 10 | inventory.setSlot(slot); 11 | } 12 | return inventory; 13 | } 14 | 15 | let tests = { 16 | 17 | testSerializeSlot: (inventory)=>{ 18 | 19 | for (slotNum in inventory.slots) { 20 | let slot = inventory.serializeSlot(slotNum); 21 | validators.inventorySlot(slot); 22 | assert(slot.side == inventory.side); 23 | assert(slot.slotNum == slotNum); 24 | assert.deepEqual(slot.contents, inventory.slots[slotNum]); 25 | } 26 | 27 | }, 28 | 29 | } 30 | 31 | runTests(tests, setup, testData); -------------------------------------------------------------------------------- /public/lua/oc/tcp.lua: -------------------------------------------------------------------------------- 1 | local internet = require('internet'); 2 | local JSON = require("json"); 3 | local config = require('config'); 4 | local conf = config.get(config.path); 5 | 6 | local handle = internet.open(conf.serverIP, tonumber(conf.tcpPort)); 7 | handle:setvbuf('line'); 8 | -- handle:setTimeout('10'); 9 | 10 | local delimiter = '\n'; 11 | 12 | local M = {}; 13 | 14 | function M.read() 15 | -- reads delimited by newlines 16 | return JSON:decode(handle:read()); 17 | end 18 | 19 | function M.write(data) 20 | local status, result = pcall(function() 21 | -- without the newline the write will wait in the buffer 22 | handle:write(JSON:encode(data)..delimiter); 23 | end); 24 | if not status then 25 | local errorMessage = {['message']='Failed to serialize result!'}; 26 | handle:write(JSON:encode(errorMessage)..delimiter); 27 | end 28 | return status; 29 | end 30 | 31 | function M.close() 32 | return handle:close(); 33 | end 34 | 35 | M.write({id={account=conf.accountName, robot=conf.robotName}}); 36 | 37 | return M; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anthony "dunstad" Blount 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /documentation/server-install.md: -------------------------------------------------------------------------------- 1 | ### Server 2 | 3 | Following these instructions will let you access the Roboserver directly from your web browser. If you want, you can even make it available over a network so your friends can too. 4 | 5 | (If this is too much trouble, you can get the desktop version of the Roboserver [here (TODO)](TODO).) 6 | 7 | 1. Install Node.js and Node Package Manager (npm). 8 | 2. Clone this repository. 9 | 3. Checkout a [released version](https://github.com/dunstad/roboserver/releases). 10 | 3. Run ```npm install``` in the project directory. 11 | 4. Rename ```public/js/config.example.js``` to ```public/js/config.js``` and optionally change the settings inside. 12 | 5. Run ```npm run server``` in the project directory. 13 | 14 | If you're running the Roboserver on the same network as any robots trying to connect to it, you may need to change the blacklist settings in your OpenComputers configuration file. For example, if you're running it on the same computer as the Minecraft world your robots are in, you'll need to remove "127.0.0.0/8" from the blacklist, otherwise your robot will be unable to connect. 15 | 16 | Congratulations, you're halfway done! Now let's get a robot set up. 17 | 18 | If you just want to try the program out without any fuss, I recommend using a Creative robot. Read how [here](creative-robot-install.md). If you want to use this in a survival world, you'll want to follow the steps [here](survival-robot-install.md). -------------------------------------------------------------------------------- /public/js/client/WorldAndScenePoint.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This class allows for easy conversion between Three.js scene coordinates and Minecraft world coordinates. 3 | */ 4 | export class WorldAndScenePoint { 5 | 6 | /** 7 | * This class allows for easy conversion between Three.js scene coordinates and Minecraft world coordinates. 8 | * @param {THREE.Vector3 | object} point 9 | * @param {boolean} isWorldPoint 10 | */ 11 | constructor(point, isWorldPoint) { 12 | 13 | this.voxelSideLength = 50; 14 | 15 | if (isWorldPoint) { 16 | this.worldPoint = new THREE.Vector3(point.x, point.y, point.z); 17 | this.scenePoint = new THREE.Vector3( 18 | point.x * this.voxelSideLength, 19 | point.y * this.voxelSideLength, 20 | point.z * this.voxelSideLength 21 | ); 22 | } 23 | 24 | else { 25 | this.scenePoint = new THREE.Vector3(point.x, point.y, point.z); 26 | this.worldPoint = new THREE.Vector3( 27 | Math.round(point.x / this.voxelSideLength), 28 | Math.round(point.y / this.voxelSideLength), 29 | Math.round(point.z / this.voxelSideLength) 30 | ); 31 | } 32 | 33 | } 34 | 35 | /** 36 | * The world point contains the coordinates of something in the Minecraft world. 37 | * @returns {THREE.Vector3} 38 | */ 39 | world() { 40 | return this.worldPoint; 41 | } 42 | 43 | /** 44 | * The scene point contains coordinates used by three.js to render meshes. 45 | * @returns {THREE.Vector3} 46 | */ 47 | scene() { 48 | return this.scenePoint; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /documentation/faq.md: -------------------------------------------------------------------------------- 1 | #### Why did you pick such weird colors for the map? Purple water? 2 | 3 | The geolyzer can only tell us the estimated hardness of blocks when scanning from a distance, not exactly what they are. Because of this, water and lava look exactly the same to the robot. There are a few ways to address this problem. I decided to make the colors of blocks halfway between the common things they could possibly be in most cases, which is why the water is purple (a mix of red for lava and blue for water). 4 | 5 | Alternatively, I could have picked the most common block at each level of hardness and used that color, but then you have situations like a desert with lava pools that's colored just like a field with ponds. That being said, I'm not against this approach if the current one turns out to be too hard to get used to. 6 | 7 | Here are all the colors in use currently, and the hardness they represent: 8 | 9 | * Bedrock: Hardness = -1, Color = Pure Black (#000000) 10 | * Leaves: Hardness = 0.2, Color = Green (#00CC00) 11 | * Glowstone: Hardness = 0.3, Color = Yellow (#FFCC00) 12 | * Netherrack: Hardness = 0.4, Color = Maroon (#800000) 13 | * Dirt or Sand: Hardness = 0.5, Color = Tan (#ffc140) 14 | * Grass Block: Hardness = 0.6, Color = Yellow Green (#ddc100) 15 | * Sandstone: Hardness = 0.8, Color = Cream (#ffff99) 16 | * Pumpkins or Melons: Hardness = 1.0, Color = Orange (#fdca00) 17 | * Smooth Stone: Hardness = 1.5, Color = Light Gray (#cfcfcf) 18 | * Cobblestone: Hardness = 2.0, Color = Dark Gray (#959595) 19 | * Ores: Hardness = 3.0, Color = Light Blue (#66ffff) 20 | * Cobwebs: Hardness = 4.0, Color = Off White (#f5f5f5) 21 | * Ore Blocks: Hardness = 5.0, Color = Red (#c60000) 22 | * Obsidian: Hardness = 50, Color = Black (#1f1f1f) 23 | * Water or Lava: Hardness = 100, Color = Purple (#9900cc) -------------------------------------------------------------------------------- /public/js/shared/recipeSearch.js: -------------------------------------------------------------------------------- 1 | function getRecipeNames(recipe) { 2 | var recipeNames = []; 3 | for (var output of recipe.out) { 4 | for (var item of output) { 5 | if (recipeNames.indexOf(item.product) == -1) { 6 | recipeNames.push(item.product); 7 | } 8 | } 9 | } 10 | return recipeNames; 11 | } 12 | 13 | function findRecipeFor(product, recipes) { 14 | var recipesForProduct = []; 15 | for (var recipe of recipes) { 16 | var recipeProducts = getRecipeNames(recipe); 17 | if (recipeProducts.indexOf(product) != -1) { 18 | recipesForProduct.push(recipe); 19 | } 20 | } 21 | return recipesForProduct; 22 | } 23 | 24 | function extractRecipeFor(product, recipe) { 25 | if (recipe.out.length == 1) { 26 | var productRecipe = recipe; 27 | } 28 | else { 29 | var indexToCraftingSlotMap = [1, 2, 3, 5, 6, 7, 9, 10, 11]; 30 | var productRecipe = {"in":{}, "out":[]}; 31 | 32 | var productIndex; 33 | for (var i = 0; i < recipe.out.length; i++) { 34 | var outputName = recipe.out[i][0].product; 35 | if (outputName == product) { 36 | productIndex = i; 37 | } 38 | } 39 | if (productIndex === undefined) {productRecipe = false;} 40 | else { 41 | for (var slot in recipe.in) { 42 | if (recipe.in[slot].length == recipe.out.length) { 43 | productRecipe.in[slot] = recipe.in[slot][productIndex].map(item=>[item]); 44 | } 45 | else { 46 | productRecipe.in[slot] = [recipe.in[slot][0]]; 47 | } 48 | } 49 | } 50 | productRecipe.out.push(recipe.out[productIndex]); 51 | } 52 | 53 | return productRecipe; 54 | } 55 | 56 | try { 57 | module.exports.findRecipeFor = findRecipeFor; 58 | module.exports.extractRecipeFor = extractRecipeFor; 59 | } 60 | catch(e) {;} -------------------------------------------------------------------------------- /public/js/client/VoxelMap.mjs: -------------------------------------------------------------------------------- 1 | import {WorldAndScenePoint} from '/js/client/WorldAndScenePoint.mjs'; 2 | 3 | /** 4 | * An organized way to store the voxels of a terrain map. 5 | */ 6 | export class VoxelMap { 7 | 8 | /** 9 | * An organized way to store the voxels of a terrain map. 10 | */ 11 | constructor() { 12 | this.map = {}; 13 | } 14 | 15 | /** 16 | * Retrieve a voxel from the map if it exists. 17 | * @param {WorldAndScenePoint} point 18 | * @returns {THREE.Mesh | false} 19 | */ 20 | get(point) { 21 | var worldPoint = point.world(); 22 | var x = worldPoint.x; 23 | var y = worldPoint.y; 24 | var z = worldPoint.z; 25 | var result; 26 | if (this.map[x] && this.map[x][y] && this.map[x][y][z]) { 27 | result = this.map[x][y][z]; 28 | } 29 | else {result = false;} 30 | return result; 31 | } 32 | 33 | /** 34 | * Store a voxel in the map or remove one from it. 35 | * @param {WorldAndScenePoint} point 36 | * @param {THREE.Mesh} voxel 37 | * @returns {THREE.Mesh} 38 | */ 39 | set(point, voxel) { 40 | var worldPoint = point.world(); 41 | var x = worldPoint.x; 42 | var y = worldPoint.y; 43 | var z = worldPoint.z; 44 | if (!this.map[x]) {this.map[x] = {};} 45 | if (!this.map[x][y]) {this.map[x][y] = {};} 46 | this.map[x][y][z] = voxel; 47 | return voxel; 48 | } 49 | 50 | /** 51 | * Call this function on every voxel in the map. 52 | * @param {function} func 53 | */ 54 | forEach(func) { 55 | for (var xIndex in this.map) { 56 | for (var yIndex in this.map[xIndex]) { 57 | for (var zIndex in this.map[xIndex][yIndex]) { 58 | var point = new WorldAndScenePoint(new THREE.Vector3(xIndex, yIndex, zIndex), true); 59 | var voxel = this.get(point); 60 | if (voxel) {func(voxel);} 61 | } 62 | } 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /public/lua/oc/adjacent.lua: -------------------------------------------------------------------------------- 1 | local orient = require('trackOrientation'); 2 | local pos = require('trackPosition'); 3 | local mas = require('moveAndScan'); 4 | 5 | local M = {}; 6 | 7 | function M.distance(coord1, coord2) 8 | local xDist = coord2.x - coord1.x; 9 | local yDist = coord2.y - coord1.y; 10 | local zDist = coord2.z - coord1.z; 11 | return math.sqrt(xDist^2 + yDist^2 + zDist^2); 12 | end 13 | 14 | function M.distFromSort(coord1) 15 | return function(coord2, coord3) 16 | local dist1 = M.distance(coord1, coord2); 17 | local dist2 = M.distance(coord1, coord3); 18 | return dist1 < dist2; 19 | end 20 | end 21 | 22 | function M.distanceSort(start, destinations) 23 | table.sort(destinations, M.distFromSort(start)); 24 | end 25 | 26 | function M.getAdjacentPoints(point) 27 | local negXPoint = {x=point.x-1, y=point.y, z=point.z}; 28 | local posXPoint = {x=point.x+1, y=point.y, z=point.z}; 29 | local posZPoint = {x=point.x, y=point.y, z=point.z+1}; 30 | local negZPoint = {x=point.x, y=point.y, z=point.z-1}; 31 | local negYPoint = {x=point.x, y=point.y-1, z=point.z}; 32 | local posYPoint = {x=point.x, y=point.y+1, z=point.z}; 33 | return {negXPoint, posXPoint, negZPoint, posZPoint, negYPoint, posYPoint}; 34 | end 35 | 36 | function M.facePoint(point) 37 | local start = pos.get(); 38 | if point.x ~= start.x then 39 | orient.faceX(point.x - start.x); 40 | elseif point.z ~= start.z then 41 | orient.faceZ(point.z - start.z); 42 | end 43 | return orient.get(); 44 | end 45 | 46 | function M.toAdjacent(point, scanType, times) 47 | local adjacentPoints = M.getAdjacentPoints(point); 48 | M.distanceSort(pos.get(), adjacentPoints); 49 | local success = false; 50 | for index, adjPoint in pairs(adjacentPoints) do 51 | if not success then 52 | success = mas.to(adjPoint.x, adjPoint.y, adjPoint.z, false, scanType, times); 53 | end 54 | end 55 | M.facePoint(point); 56 | orient.save(); 57 | return success; 58 | end 59 | 60 | return M; 61 | -------------------------------------------------------------------------------- /public/lua/oc/sendScan.lua: -------------------------------------------------------------------------------- 1 | local component = require('component'); 2 | if not component.isAvailable("geolyzer") then 3 | error("Geolyzer not found"); 4 | end 5 | local geolyzer = component.geolyzer; 6 | tcp = require('tcp'); -- if this is local, reloading modules fails in commandLoop 7 | local pos = require('trackPosition'); 8 | 9 | local M = {}; 10 | 11 | function M.weightedAverage(n1, w1, n2, w2) 12 | return (n1*w1 + n2*w2)/(w1 + w2); 13 | end 14 | 15 | -- round a number to 2 decimal places 16 | function round(num) return tonumber(string.format("%." .. 2 .. "f", num)) end 17 | 18 | function M.volume(x, z, y, w, d, h, times) 19 | -- default to 0 (which is true in lua) 20 | if times then 21 | times = times - 1; 22 | else 23 | times = 0; 24 | end 25 | 26 | local robotPos = pos.get(); 27 | local result = { 28 | x = x + robotPos.x, 29 | y = y + robotPos.y, 30 | z = z + robotPos.z, 31 | w=w, 32 | d=d, 33 | data=geolyzer.scan(x, z, y, w, d, h)}; 34 | 35 | local weight = 1; 36 | for i = 1, times do 37 | 38 | local newScan = geolyzer.scan(x, z, y, w, d, h); 39 | 40 | -- average all data points using weights 41 | for j = 1, result.data.n do 42 | result.data[j] = M.weightedAverage(result.data[j], weight, newScan[j], 1); 43 | end 44 | 45 | weight = weight + 1; 46 | 47 | end 48 | 49 | -- round the numbers to save space in the json 50 | -- important because robots have a limited write buffer 51 | for i = 1, result.data.n do 52 | result.data[i] = round(result.data[i]); 53 | end 54 | 55 | tcp.write({['map data']=result}); 56 | return result; 57 | end 58 | 59 | function M.plane(y, times) 60 | for x = -32, 32 do 61 | M.volume(x, -32, y, 1, 64, 1, times); 62 | end 63 | -- max shape volume is 64, but we can scan from -32 to 32, inclusive 64 | -- that's 65, so we have one row we miss in the previous loop to scan 65 | -- still missing one cube after this final row, but oh well 66 | M.volume(-32, 32, y, 64, 1, 1, times); 67 | return true; 68 | end 69 | 70 | return M; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roboserver", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "node ./public/js/server/robotTest.js & node ./public/js/server/inventoryTest.js", 7 | "start": "node ./bin/www", 8 | "server": "node ./bin/www", 9 | "server-dev": "nodemon ./bin/www", 10 | "connect": "nodemon ./public/js/server/connect.js", 11 | "electron": "electron electronApp.js", 12 | "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=public/assets/placeholder_icon.icns --prune=true --out=release-builds", 13 | "package-win": "electron-packager . --overwrite --platform=win32 --arch=x64 --icon=public/assets/placeholder_icon.ico --prune=true --out=release-builds", 14 | "package-all": "electron-packager . --overwrite --all --icon=public/assets/placeholder_icon.icns --prune=true --out=release-builds" 15 | }, 16 | "dependencies": { 17 | "ajv": "^6.10.2", 18 | "bcryptjs": "^2.4.3", 19 | "body-parser": "^1.15.2", 20 | "cookie-parser": "~1.4.3", 21 | "debug": "^4.1.1", 22 | "ejs": "^2.5.2", 23 | "express": "^4.16.4", 24 | "express-session": "^1.15.2", 25 | "minecraft-data": "^2.34.0", 26 | "morgan": "^1.9.1", 27 | "nedb": "^1.8.0", 28 | "nedb-promise": "^2.0.1", 29 | "nedb-session-store": "^1.1.1", 30 | "passport": "^0.3.2", 31 | "passport-local": "^1.0.0", 32 | "passport.socketio": "^3.7.0", 33 | "serve-favicon": "^2.5.0", 34 | "socket.io": "^2.2.0" 35 | }, 36 | "devDependencies": { 37 | "electron": "^3.0.10", 38 | "electron-packager": "^12.2.0", 39 | "nodemon": "^1.18.6" 40 | }, 41 | "main": "electronApp.js", 42 | "description": "This is a HTTP and TCP server which OpenComputers robots can read and execute commands from.", 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/dunstad/roboserver.git" 46 | }, 47 | "author": "dunstad", 48 | "license": "ISC", 49 | "bugs": { 50 | "url": "https://github.com/dunstad/roboserver/issues" 51 | }, 52 | "homepage": "https://github.com/dunstad/roboserver#readme" 53 | } 54 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('roboserver:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || require('../public/js/config/config.js').webServerPort); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | // add socket.io handlers and create tcp server 25 | require('../public/js/server/customizeServer.js')(server, app); 26 | 27 | /** 28 | * Listen on provided port, on all network interfaces. 29 | */ 30 | 31 | server.listen(port); 32 | server.on('error', onError); 33 | server.on('listening', onListening); 34 | 35 | /** 36 | * Normalize a port into a number, string, or false. 37 | */ 38 | 39 | function normalizePort(val) { 40 | var port = parseInt(val, 10); 41 | 42 | if (isNaN(port)) { 43 | // named pipe 44 | return val; 45 | } 46 | 47 | if (port >= 0) { 48 | // port number 49 | return port; 50 | } 51 | 52 | return false; 53 | } 54 | 55 | /** 56 | * Event listener for HTTP server "error" event. 57 | */ 58 | 59 | function onError(error) { 60 | if (error.syscall !== 'listen') { 61 | throw error; 62 | } 63 | 64 | var bind = typeof port === 'string' 65 | ? 'Pipe ' + port 66 | : 'Port ' + port; 67 | 68 | // handle specific listen errors with friendly messages 69 | switch (error.code) { 70 | case 'EACCES': 71 | console.error(bind + ' requires elevated privileges'); 72 | process.exit(1); 73 | break; 74 | case 'EADDRINUSE': 75 | console.error(bind + ' is already in use'); 76 | process.exit(1); 77 | break; 78 | default: 79 | throw error; 80 | } 81 | } 82 | 83 | /** 84 | * Event listener for HTTP server "listening" event. 85 | */ 86 | 87 | function onListening() { 88 | var addr = server.address(); 89 | var bind = typeof addr === 'string' 90 | ? 'pipe ' + addr 91 | : 'port ' + addr.port; 92 | debug('Listening on ' + bind); 93 | } 94 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Roboserver 2 | 3 | This project lets you control [OpenComputers](http://ocdoc.cil.li/) robots through a simple GUI. No Lua coding necessary! 4 | 5 | ![A robot being controlled by the Roboserver](public/assets/tree.gif) 6 | 7 | Here are a few things the Roboserver can simplify right now: 8 | * Digging large areas 9 | * Crafting complex items 10 | * Repetitive construction 11 | * Locating ore underground 12 | * [Other stuff!](https://www.youtube.com/watch?v=2lbb0-yfSdw) 13 | 14 | ## Getting Started 15 | 16 | (Tested on Minecraft version 1.10.2, OpenComputers version 1.6. If something's broken for your version, see [Reporting Bugs](#reporting-bugs).) 17 | 18 | If you're looking to get set up as fast as possible, you can get the desktop version of the Roboserver [here (TODO)](TODO). See how to start using it [here](documentation/standalone-install.md). 19 | 20 | Alternatively, if you've got a bit of technical know-how and more time on your hands, you can run the Roboserver from the source code by following the instructions [here](documentation/server-install.md). 21 | 22 | ## Reporting Bugs 23 | 24 | Before creating an issue, please read the [usage tips](documentation/tips.md), and check that it hasn't already been reported. 25 | 26 | When reporting a bug in the [issue tracker](https://github.com/dunstad/roboserver/issues?q=is%3Aopen), in order to help me address your issue as quickly as possible, please provide the following information: 27 | 28 | 1. What version you're using of Minecraft and OpenComputers 29 | 2. Whether you're using the Roboserver's desktop application or accessing it from your browser 30 | * If you're using a browser, state which one 31 | 3. Steps to reproduce the problem 32 | 4. The expected behavior 33 | 5. The actual behavior 34 | 35 | Screenshots, video, and error messages are always welcome. 36 | 37 | Feel free to create a pull request if you've resolved an outstanding issue. 38 | 39 | ## License 40 | 41 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 42 | 43 | ## Acknowledgments 44 | 45 | This project is made possible by the continued effort of all the wonderful people who contribute to [OpenComputers](https://github.com/MightyPirates/OpenComputers). -------------------------------------------------------------------------------- /documentation/survival-robot-install.md: -------------------------------------------------------------------------------- 1 | #### Survival 2 | 3 | If you have no previous experience with OpenComputers, you should take a look at [this guide](http://ocdoc.cil.li/tutorial:oc1_basic_computer). 4 | 5 | You need to craft at minimum the following parts for your robot: 6 | * [Computer Case (Tier 2)](http://crafting-guide.com/browse/opencomputers/computer_case_tier_2/) 7 | * [EEPROM (Lua BIOS)](http://crafting-guide.com/browse/opencomputers/eeprom_lua_bios/) 8 | * [CPU (Tier 2)](http://crafting-guide.com/browse/opencomputers/central_processing_unit_cpu_tier_2/) 9 | * [Memory (Tier 1)](http://crafting-guide.com/browse/opencomputers/memory_tier_1/) x2 10 | * [Hard Disk Drive (Tier 1)](http://crafting-guide.com/browse/opencomputers/hard_disk_drive_tier_1/) with [OpenOS](http://crafting-guide.com/browse/opencomputers/floppy_disk_openos/) installed 11 | * [Internet Card](http://crafting-guide.com/browse/opencomputers/internet_card/) 12 | * [Geolyzer](http://crafting-guide.com/browse/opencomputers/geolyzer/) 13 | * [Inventory Upgrade](http://crafting-guide.com/browse/opencomputers/inventory_upgrade/) 14 | * [Inventory Controller Upgrade](http://crafting-guide.com/browse/opencomputers/inventory_controller_upgrade/) 15 | * [Crafting Upgrade](http://crafting-guide.com/browse/opencomputers/crafting_upgrade/) 16 | 17 | Unless you really know what you're doing, you probably need these too: 18 | * [Keyboard](http://crafting-guide.com/browse/opencomputers/keyboard/) 19 | * [Screen](http://crafting-guide.com/browse/opencomputers/screen_tier_1/) 20 | * [Graphics Card (Tier 1)](http://crafting-guide.com/browse/opencomputers/graphics_card_tier_1/) 21 | 22 | Place all these parts in an [Electronics Assembler](http://crafting-guide.com/browse/opencomputers/electronics_assembler/). Power and start the assembler, and when your robot is finished, place it in the world and power it on. 23 | 24 | Now that your robot is running and OpenOS is installed, just paste [this](web_install.txt) into it. 25 | (If your Roboserver is running but you don't have internet access, you can use the [local install](local_install.txt) instead.) 26 | 27 | After answering a few questions about your robot, it will connect to the server you started in the previous step. Congratulations, you're done! Next check out [these tips](tips.md) on how to use the Roboserver. -------------------------------------------------------------------------------- /public/js/client/CoordForm.mjs: -------------------------------------------------------------------------------- 1 | import {WorldAndScenePoint} from '/js/client/WorldAndScenePoint.mjs'; 2 | 3 | /** 4 | * An organized way to access coordinates stored in number inputs. 5 | */ 6 | export class CoordForm { 7 | 8 | /** 9 | * An organized way to access coordinates stored in number inputs. 10 | * @param {HTMLInputElement} xForm 11 | * @param {HTMLInputElement} yForm 12 | * @param {HTMLInputElement} zForm 13 | */ 14 | constructor(xForm, yForm, zForm) { 15 | this.x = xForm; 16 | this.y = yForm; 17 | this.z = zForm; 18 | } 19 | 20 | /** 21 | * Used to tell when no coordinates are entered. 22 | * @returns {boolean} 23 | */ 24 | isEmpty() { 25 | var empty = false; 26 | if (!(this.x.value || this.y.value || this.z.value )) { 27 | empty = true; 28 | } 29 | return empty; 30 | } 31 | 32 | /** 33 | * Used to tell when all coordinates have been entered. 34 | * @returns {boolean} 35 | */ 36 | isComplete() { 37 | var complete = false; 38 | if (this.x.value && this.y.value && this.z.value ) { 39 | complete = true; 40 | } 41 | return complete; 42 | } 43 | 44 | /** 45 | * Creates a Vector3 from the current form input. 46 | * @returns {WorldAndScenePoint} 47 | */ 48 | getPoint() { 49 | return new WorldAndScenePoint( 50 | new THREE.Vector3( 51 | parseInt(this.x.value), 52 | parseInt(this.y.value), 53 | parseInt(this.z.value) 54 | ), 55 | true 56 | ); 57 | } 58 | 59 | /** 60 | * Sets form inputs to the values in the provided point. 61 | * @param {WorldAndScenePoint} point 62 | */ 63 | setFromPoint(point) { 64 | var worldVector = point.world(); 65 | this.x.value = worldVector.x; 66 | this.y.value = worldVector.y; 67 | this.z.value = worldVector.z; 68 | } 69 | 70 | /** 71 | * Removes any values from all fields of the form. 72 | */ 73 | clear() { 74 | this.x.value = ""; 75 | this.y.value = ""; 76 | this.z.value = ""; 77 | } 78 | 79 | /** 80 | * Used to add one event listener to all inputs. 81 | * @param {string} type 82 | * @param {function} listener 83 | */ 84 | addEventListener(type, listener) { 85 | this.x.addEventListener(type, listener); 86 | this.y.addEventListener(type, listener); 87 | this.z.addEventListener(type, listener); 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /public/lua/oc/scanDirection.lua: -------------------------------------------------------------------------------- 1 | local orient = require('trackOrientation'); 2 | local scan = require('sendScan'); 3 | 4 | local M = {}; 5 | 6 | function M.makeBigScanner(x, z, w, d) 7 | return function(y, times) 8 | return scan.volume(x, z, y, w, d, 1, times); 9 | end; 10 | end 11 | 12 | -- functions to scan one row at the end of an axis 13 | local scanZPosBig = M.makeBigScanner(-32, 32, 64, 1); 14 | local scanZNegBig = M.makeBigScanner(-32, -32, 64, 1); 15 | local scanXPosBig = M.makeBigScanner(32, -32, 1, 64); 16 | local scanXNegBig = M.makeBigScanner(-32, -32, 1, 64); 17 | 18 | local scanBigMap = { 19 | [0]=scanZPosBig, 20 | [1]=scanZNegBig, 21 | [2]=scanXPosBig, 22 | [3]=scanXNegBig 23 | }; 24 | 25 | function M.makeSmallScanner(x, z, w, d) 26 | return function(times) 27 | return scan.volume(x, z, -2, w, d, 8, times); 28 | end; 29 | end 30 | 31 | -- scan.volume(-3, -3, 8, 8) 32 | -- functions to scan a small plane in a particular direction 33 | local scanZPosSmall = M.makeSmallScanner(-3, 4, 8, 1); 34 | local scanZNegSmall = M.makeSmallScanner(-3, -3, 8, 1); 35 | local scanXPosSmall = M.makeSmallScanner(4, -3, 1, 8); 36 | local scanXNegSmall = M.makeSmallScanner(-3, -3, 1, 8); 37 | 38 | local scanSmallMap = { 39 | [0]=scanZPosSmall, 40 | [1]=scanZNegSmall, 41 | [2]=scanXPosSmall, 42 | [3]=scanXNegSmall 43 | }; 44 | 45 | 46 | local scanForwardMap = { 47 | [0]=0, 48 | [1]=2, 49 | [2]=1, 50 | [3]=3 51 | }; 52 | 53 | local scanBackMap = { 54 | [0]=1, 55 | [1]=3, 56 | [2]=0, 57 | [3]=2 58 | }; 59 | 60 | -- orientation is from trackOrientation.lua 61 | function M.forwardBig(y, times) 62 | return scanBigMap[scanForwardMap[orient.get()]](y, times); 63 | end 64 | 65 | function M.backBig(y, times) 66 | return scanBigMap[scanBackMap[orient.get()]](y, times); 67 | end 68 | 69 | function M.upBig(times) 70 | return scan.plane(7); 71 | end; 72 | 73 | function M.downBig(times) 74 | return scan.plane(-1); 75 | end; 76 | 77 | function M.forwardSmall(times) 78 | return scanSmallMap[scanForwardMap[orient.get()]](y, times); 79 | end 80 | 81 | function M.backSmall(times) 82 | return scanSmallMap[scanBackMap[orient.get()]](y, times); 83 | end 84 | 85 | function M.upSmall(times) 86 | return scan.volume(-3, -3, 5, 8, 8, 1, times); 87 | end 88 | 89 | function M.downSmall(times) 90 | return scan.volume(-3, -3, -2, 8, 8, 1, times); 91 | end 92 | 93 | return M; 94 | -------------------------------------------------------------------------------- /public/lua/oc/commandLoop.lua: -------------------------------------------------------------------------------- 1 | -- packages that use tcp 2 | function reloadPackages() 3 | tcp = require('tcp'); 4 | commandMap = require('commandMap'); 5 | dta = require('doToArea'); 6 | int = require('interact'); 7 | sendScan = require('sendScan'); 8 | pos = require('trackPosition'); 9 | end 10 | 11 | function reconnect(sleepTime) 12 | tcp.close(); 13 | -- unloading 'computer' breaks stuff, it can't be required again for some reason 14 | -- unload all packages that use tcp so they work after reconnecting 15 | local loadedPackages = {'tcp', 'commandMap', 'doToArea', 'interact', 'sendScan', 'trackPosition'}; 16 | for index, p in pairs(loadedPackages) do 17 | package.loaded[p] = nil; 18 | end 19 | -- wait for server to come back up 20 | os.sleep(sleepTime or 5); 21 | -- reconnect to server 22 | reloadPackages(); 23 | end 24 | 25 | function loadSafely() 26 | commandMap = require('commandMap'); 27 | dta = require('doToArea'); 28 | int = require('interact'); 29 | sendScan = require('sendScan'); 30 | pos = require('trackPosition'); 31 | scanDirection = require('scanDirection'); 32 | orient = require('trackOrientation'); 33 | mas = require('moveAndScan'); 34 | robot = require('robot'); 35 | adj = require('adjacent'); 36 | craft = require('craft'); 37 | computer = require('computer'); 38 | config = require('config'); 39 | raw = config.get(config.path).raw; 40 | end 41 | 42 | -- not much we can do if tcp fails 43 | -- it shouldn't change much though 44 | tcp = require('tcp'); 45 | local success, message = pcall(loadSafely); 46 | if not success then 47 | print(message); 48 | tcp.write({['message']=message}); 49 | reconnect(15); 50 | end 51 | 52 | function unpack (t, i) 53 | i = i or 1; 54 | if t[i] ~= nil then 55 | return t[i], unpack(t, i + 1); 56 | end 57 | end 58 | 59 | -- wait until a command exists, grab it, execute it, and send result back 60 | function executeCommand() 61 | local data = tcp.read(); 62 | local result = commandMap[data['name']](unpack(data['parameters'])); 63 | tcp.write({['command result']={data['name'], result}}); 64 | tcp.write({['power level']=computer.energy()/computer.maxEnergy()}); 65 | end 66 | 67 | continueLoop = true; 68 | while continueLoop do 69 | local success, message = pcall(executeCommand); 70 | if not success then 71 | print(message); 72 | tcp.write({['message']=message}); 73 | reconnect(); 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /public/js/server/webClientTestData.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 'listen start': { 4 | robot: 'rob', 5 | }, 6 | 7 | 'listen end': { 8 | robot: 'rob', 9 | }, 10 | 11 | 'command result': { 12 | robot: 'rob', 13 | data: [true, 'a test command result'], 14 | }, 15 | 16 | 'map data': { 17 | robot: 'rob', 18 | data: { 19 | x: 0, 20 | z: 0, 21 | y: 0, 22 | w: 3, 23 | d: 3, 24 | data: { 25 | 1: 1, 26 | 2: 1, 27 | 3: 1, 28 | 4: 1, 29 | 5: 1, 30 | 6: 1, 31 | 7: 1, 32 | 8: 1, 33 | 9: 1, 34 | 10: 1, 35 | 11: 1, 36 | 12: 1, 37 | 13: 1, 38 | 14: 0, 39 | 15: 1, 40 | 16: 1, 41 | 17: 1, 42 | 18: 1, 43 | 19: 1, 44 | 20: 1, 45 | 21: 1, 46 | 22: 1, 47 | 23: 0, 48 | 24: 1, 49 | 25: 1, 50 | 26: 1, 51 | 27: 1, 52 | n: 27 53 | }, 54 | }, 55 | }, 56 | 57 | 58 | 'block data': { 59 | robot: 'rob', 60 | data: { 61 | name: 'minecraft:dirt', 62 | hardness: .5, 63 | point: { 64 | x: 2, 65 | y: 2, 66 | z: 2, 67 | }, 68 | }, 69 | }, 70 | 71 | 'robot position': { 72 | robot: 'rob', 73 | data: { 74 | x: 4, 75 | y: 4, 76 | z: 4, 77 | }, 78 | }, 79 | 80 | 'delete selection': { 81 | robot: 'rob', 82 | data: 1, 83 | }, 84 | 85 | 'dig success': { 86 | robot: 'rob', 87 | data: { 88 | x: 2, 89 | y: 2, 90 | z: 2, 91 | }, 92 | }, 93 | 94 | 'inventory data': { 95 | robot: 'rob', 96 | data: { 97 | 'size': 64, 98 | 'side': -1, 99 | 'selected': 1 100 | }, 101 | }, 102 | 103 | 'slot data': { 104 | robot: 'rob', 105 | data: { 106 | side: -1, 107 | slotNum: 1, 108 | contents: { 109 | damage: 0, 110 | hasTag: false, 111 | label: 'Dirt', 112 | maxDamage: 0, 113 | maxSize: 64, 114 | name: 'minecraft:dirt', 115 | size: 64 116 | } 117 | }, 118 | }, 119 | 120 | 'power level': { 121 | robot: 'rob', 122 | data: .5, 123 | }, 124 | 125 | 'available components': { 126 | robot: 'rob', 127 | data: {raw: true}, 128 | }, 129 | 130 | }; -------------------------------------------------------------------------------- /public/lua/oc/trackOrientation.lua: -------------------------------------------------------------------------------- 1 | local robot = require('robot'); 2 | local math = require('math'); 3 | local config = require('config'); 4 | 5 | local orientation = tonumber(config.get(config.path).orient); 6 | -- 0: z+, south 7 | -- 1: x+, east 8 | -- 2: z-, north 9 | -- 3: x-, west 10 | 11 | local axisMap = { 12 | x = { 13 | pos = 1, 14 | neg = 3 15 | }, 16 | z = { 17 | pos = 0, 18 | neg = 2 19 | } 20 | }; 21 | 22 | local M = {}; 23 | 24 | function M.save() 25 | config.set({orient=orientation}, config.path); 26 | end 27 | 28 | function M.load() 29 | orientation = config.get(config.path).orient; 30 | end 31 | 32 | function M.set(orient) 33 | orientation = orient; 34 | end 35 | 36 | function M.get() 37 | return orientation; 38 | end 39 | 40 | -- start using these functions when the robot is facing south. 41 | -- don't revert to using normal turn functions or they'll stop being accurate 42 | function M.turnLeft() 43 | robot.turnLeft(); 44 | orientation = (orientation + 1) % 4; 45 | return orientation; 46 | end 47 | 48 | function M.turnRight() 49 | robot.turnRight(); 50 | orientation = (orientation - 1) % 4; 51 | return orientation; 52 | end 53 | 54 | -- accepts number of times to turn, pos for left, neg for right 55 | function M.turn(num) 56 | local turnFunction; 57 | if num > 0 then 58 | turnFunction = M.turnLeft; 59 | else 60 | turnFunction = M.turnRight; 61 | end 62 | 63 | for i = 1, math.abs(num) do 64 | turnFunction(); 65 | end 66 | return orientation; 67 | end 68 | 69 | -- accepts direction to face 70 | function M.face(num) 71 | assert(num >= 0 and num <= 3, 'facing out of range'); 72 | if num == orientation then return orientation; end 73 | local distR = num - orientation; 74 | -- should never have to turn more than twice 75 | if math.abs(distR) > 2 then 76 | local distRSign = distR / math.abs(distR); 77 | local distL = distR - 4 * distRSign; 78 | return M.turn(distL); 79 | else 80 | return M.turn(distR); 81 | end 82 | end 83 | 84 | -- positive number to face x+, negative to face x- 85 | function M.faceX(num) 86 | local sign = num / math.abs(num); 87 | if sign > 0 then 88 | M.face(axisMap.x.pos); 89 | else 90 | return M.face(axisMap.x.neg); 91 | end 92 | end 93 | 94 | function M.faceZ(num) 95 | local sign = num / math.abs(num); 96 | if sign > 0 then 97 | M.face(axisMap.z.pos); 98 | else 99 | return M.face(axisMap.z.neg); 100 | end 101 | end 102 | 103 | return M; 104 | -------------------------------------------------------------------------------- /public/js/client/CutawayForm.mjs: -------------------------------------------------------------------------------- 1 | import {WorldAndScenePoint} from '/js/client/WorldAndScenePoint.mjs'; 2 | 3 | /** 4 | * An organized way to access how we want to cut away the map. 5 | */ 6 | export class CutawayForm { 7 | 8 | /** 9 | * An organized way to access how we want to cut away the map. 10 | * @param {HTMLButtonElement} axisForm 11 | * @param {HTMLButtonElement} operationForm 12 | * @param {HTMLInputElement} cutawayValueForm 13 | */ 14 | constructor(axisForm, operationForm, cutawayValueForm) { 15 | this.axis = axisForm; 16 | this.operation = operationForm; 17 | this.cutawayValue = cutawayValueForm; 18 | 19 | this.axis.states = ['X', 'Y', 'Z']; 20 | this.operation.states = ['>', '<']; 21 | 22 | var changeState = (e)=>{ 23 | var button = e.target; 24 | var currentState = button.states.indexOf(button.textContent); 25 | var nextState = currentState == button.states.length - 1 ? 0 : currentState + 1; 26 | button.textContent = button.states[nextState]; 27 | }; 28 | 29 | this.axis.addEventListener('click', changeState); 30 | this.operation.addEventListener('click', changeState); 31 | 32 | } 33 | 34 | /** 35 | * Removes any values from the form. 36 | */ 37 | clear() { 38 | this.cutawayValue.value = ""; 39 | } 40 | 41 | /** 42 | * Used to re-render the cutaway when the form is interacted with. 43 | * @param {function} listener 44 | */ 45 | addChangeListener(listener) { 46 | this.axis.addEventListener('click', listener); 47 | this.operation.addEventListener('click', listener); 48 | this.cutawayValue.addEventListener('input', listener); 49 | } 50 | 51 | /** 52 | * Lets us know whether a voxel should be displayed given the entered cutaway point. 53 | * @param {WorldAndScenePoint} point 54 | * @returns {boolean} 55 | */ 56 | shouldBeRendered(point) { 57 | var axisName = this.axis.textContent; 58 | var operationName = this.operation.textContent; 59 | var cutawayValue = parseInt(this.cutawayValue.value); 60 | var result = true; 61 | if (!isNaN(cutawayValue)) { 62 | var axisNameMap = { 63 | 'X': 'x', 64 | 'Y': 'y', 65 | 'Z': 'z' 66 | } 67 | var coord = point.world()[axisNameMap[axisName]]; 68 | if (operationName == '>') { 69 | if (coord > cutawayValue) { 70 | result = false; 71 | } 72 | } 73 | else if (operationName == '<') { 74 | if (coord < cutawayValue) { 75 | result = false; 76 | } 77 | } 78 | } 79 | return result; 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /electronApp.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron') 2 | // Module to control application life. 3 | const electronApp = electron.app 4 | // Module to create native browser window. 5 | const BrowserWindow = electron.BrowserWindow 6 | 7 | // for making the refresh key work 8 | const globalShortcut = electron.globalShortcut 9 | 10 | const path = require('path') 11 | const url = require('url') 12 | 13 | // Keep a global reference of the window object, if you don't, the window will 14 | // be closed automatically when the JavaScript object is garbage collected. 15 | let mainWindow 16 | 17 | function createWindow () { 18 | 19 | // start the web server 20 | require('./bin/www') 21 | 22 | // Create the browser window. 23 | mainWindow = new BrowserWindow({ 24 | width: 800, 25 | height: 600, 26 | webPreferences: { 27 | nodeIntegration: false, 28 | }, 29 | }) 30 | 31 | // and load the index.html of the app. 32 | var webServerPort = require('./public/js/config/config.js').webServerPort; 33 | mainWindow.loadURL('http://127.0.0.1:' + webServerPort + '/login') 34 | 35 | // Open the DevTools. 36 | mainWindow.webContents.openDevTools() 37 | 38 | // make the refresh key work 39 | globalShortcut.register('f5', function() { 40 | console.log('f5 is pressed') 41 | mainWindow.reload() 42 | }) 43 | globalShortcut.register('CommandOrControl+R', function() { 44 | console.log('CommandOrControl+R is pressed') 45 | mainWindow.reload() 46 | }) 47 | 48 | // Emitted when the window is closed. 49 | mainWindow.on('closed', function () { 50 | // Dereference the window object, usually you would store windows 51 | // in an array if your app supports multi windows, this is the time 52 | // when you should delete the corresponding element. 53 | mainWindow = null 54 | }) 55 | } 56 | 57 | // This method will be called when Electron has finished 58 | // initialization and is ready to create browser windows. 59 | // Some APIs can only be used after this event occurs. 60 | electronApp.on('ready', ()=>{setTimeout(createWindow, 1000)}) 61 | 62 | // Quit when all windows are closed. 63 | electronApp.on('window-all-closed', function () { 64 | // On OS X it is common for applications and their menu bar 65 | // to stay active until the user quits explicitly with Cmd + Q 66 | if (process.platform !== 'darwin') { 67 | electronApp.quit() 68 | } 69 | }) 70 | 71 | electronApp.on('activate', function () { 72 | // On OS X it's common to re-create a window in the app when the 73 | // dock icon is clicked and there are no other windows open. 74 | if (mainWindow === null) { 75 | createWindow() 76 | } 77 | }) -------------------------------------------------------------------------------- /public/js/shared/MapData.js: -------------------------------------------------------------------------------- 1 | let validators = require('./fromRobotSchemas.js').validators; 2 | /** 3 | * An organized way to store map data. 4 | */ 5 | class MapData { 6 | 7 | /** 8 | * An organized way to store map data. 9 | */ 10 | constructor() { 11 | this.map = {}; 12 | } 13 | 14 | /** 15 | * Retrieve block data from the map if it exists. 16 | * @param {number} x 17 | * @param {number} y 18 | * @param {number} z 19 | * @returns {object | false} 20 | */ 21 | get(x, y, z) { 22 | let result; 23 | if (this.map[x] && this.map[x][y] && this.map[x][y][z]) { 24 | result = this.map[x][y][z]; 25 | } 26 | else {result = false;} 27 | return result; 28 | } 29 | 30 | /** 31 | * Store block data in or remove it from the map. 32 | * This will only change properties listed in blockData. 33 | * If blockData is falsy, it removes the entry. 34 | * @param {number} x 35 | * @param {number} y 36 | * @param {number} z 37 | * @param {object} blockData 38 | * @returns {object} 39 | */ 40 | set(x, y, z, blockData) { 41 | if (!this.map[x]) {this.map[x] = {};} 42 | if (!this.map[x][y]) {this.map[x][y] = {};} 43 | if (blockData) { 44 | if (!this.map[x][y][z]) {this.map[x][y][z] = {};} 45 | Object.assign(this.map[x][y][z], blockData); 46 | } 47 | else { 48 | this.map[x][y][z] = undefined; 49 | } 50 | return blockData; 51 | } 52 | 53 | /** 54 | * Store block data contained in the geolyzer scan format 55 | * @param {object} geolyzerScan 56 | */ 57 | setFromGeolyzerScan(geolyzerScan) { 58 | validators.geolyzerScan(geolyzerScan); 59 | for (let x = 0; x < geolyzerScan.w; x++) { 60 | for (let z = 0; z < geolyzerScan.d; z++) { 61 | for (let y = 0; y < (geolyzerScan.data.n / (geolyzerScan.w * geolyzerScan.d)); y++) { 62 | 63 | let xWithOffset = x + geolyzerScan.x; 64 | let yWithOffset = y + geolyzerScan.y; 65 | let zWithOffset = z + geolyzerScan.z; 66 | 67 | // this is how the geolyzer reports 3d data in a 1d array 68 | // also lua is indexed from 1 69 | let index = (x + 1) + z*geolyzerScan.w + y*geolyzerScan.w*geolyzerScan.d; 70 | 71 | this.set(xWithOffset, yWithOffset, zWithOffset, {"hardness": geolyzerScan.data[index]}); 72 | 73 | } 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Store block data contained in the map data format 80 | * @param {object} mapData 81 | */ 82 | setFromMapData(mapData) { 83 | for (var xIndex in mapData) { 84 | for (var yIndex in mapData[xIndex]) { 85 | for (var zIndex in mapData[xIndex][yIndex]) { 86 | let blockData = mapData[xIndex][yIndex][zIndex]; 87 | this.set(xIndex, yIndex, zIndex, blockData); 88 | } 89 | } 90 | } 91 | } 92 | 93 | } 94 | 95 | try {module.exports = MapData;} 96 | catch(e) {;} -------------------------------------------------------------------------------- /public/lua/oc/trackPosition.lua: -------------------------------------------------------------------------------- 1 | local robot = require('robot'); 2 | tcp = require('tcp'); -- if this is local, reloading modules fails in commandLoop 3 | local orient = require('trackOrientation'); 4 | local config = require('config'); 5 | local confOptions = config.get(config.path); 6 | 7 | local position = { 8 | x = tonumber(confOptions.posX), 9 | y = tonumber(confOptions.posY), 10 | z = tonumber(confOptions.posZ), 11 | }; 12 | local M = {}; 13 | 14 | function M.save() 15 | local posConf = { 16 | posX = position.x, 17 | posY = position.y, 18 | posZ = position.z, 19 | }; 20 | config.set(posConf, config.path); 21 | end 22 | 23 | function M.load() 24 | local confOptions = config.get(config.path); 25 | position = { 26 | x = tonumber(confOptions.posX), 27 | y = tonumber(confOptions.posY), 28 | z = tonumber(confOptions.posZ), 29 | }; 30 | end 31 | 32 | function M.set(x, y, z) 33 | position = {x=x, y=y, z=z}; 34 | return position; 35 | end 36 | 37 | function M.get() 38 | return position; 39 | end 40 | 41 | function M.toAbsolute(x, y, z) 42 | return x + position.x, y + position.y, z + position.z; 43 | end 44 | 45 | -- how to change coordinates based on orientation 46 | -- 0: z+, south 47 | -- 1: x+, east 48 | -- 2: z-, north 49 | -- 3: x-, west 50 | local forwardMap = { 51 | [0]={z=1}, 52 | [1]={x=1}, 53 | [2]={z=-1}, 54 | [3]={x=-1} 55 | }; 56 | 57 | local backwardMap = { 58 | [0]={z=-1}, 59 | [1]={x=-1}, 60 | [2]={z=1}, 61 | [3]={x=1} 62 | }; 63 | 64 | 65 | function M.sendLocation() 66 | return tcp.write({['robot position']=position}); 67 | end 68 | 69 | -- don't stop using these functions once you start or they won't be accurate 70 | 71 | -- orientation comes from trackOrientation.lua 72 | function M.forward() 73 | -- the loop will only perform one iteration 74 | -- this is just a way to treat the properties generically 75 | if (robot.forward()) then 76 | for axis, change in pairs(forwardMap[orient.get()]) do 77 | position[axis] = position[axis] + change; 78 | end 79 | M.sendLocation(); 80 | return position; 81 | end 82 | -- if the movement failed 83 | return false; 84 | end 85 | 86 | function M.back() 87 | if (robot.back()) then 88 | for axis, change in pairs(backwardMap[orient.get()]) do 89 | position[axis] = position[axis] + change; 90 | end 91 | M.sendLocation(); 92 | return position; 93 | end 94 | return false; 95 | end 96 | 97 | function M.up() 98 | if (robot.up()) then 99 | position.y = position.y + 1; 100 | M.sendLocation(); 101 | return position; 102 | end 103 | return false; 104 | end 105 | 106 | function M.down() 107 | if (robot.down()) then 108 | position.y = position.y - 1; 109 | M.sendLocation(); 110 | return position; 111 | end 112 | return false; 113 | end 114 | 115 | return M; 116 | -------------------------------------------------------------------------------- /public/js/shared/fromClientSchemas.js: -------------------------------------------------------------------------------- 1 | if (!Ajv) {var Ajv = require("ajv")}; 2 | const ajv = Ajv({allErrors: true, $data: true}); 3 | 4 | /** 5 | * Used to automate adding this outside bit to all command schemas 6 | * and adding their validator to module.exports. 7 | * @param {object} ajv 8 | * @param {object} innerSchema 9 | * @param {string} id 10 | * @return {object} 11 | */ 12 | function makeCommandValidator(ajv, innerSchema, id, validators) { 13 | 14 | ajv.addSchema(innerSchema, id); 15 | 16 | let schema = { 17 | "properties": { 18 | "command": {"$ref": id}, 19 | "robot": {"type": "string"}, 20 | }, 21 | "additionalProperties": false, 22 | "required": ["command", "robot"], 23 | }; 24 | 25 | let result = ajv.compile(schema); 26 | 27 | validators[id] = result; 28 | 29 | return result; 30 | 31 | } 32 | 33 | /** 34 | * Used to determine minimum array size by counting optional arguments. 35 | * @param {Array[]} typeList 36 | */ 37 | function countNull(typeList) { 38 | return typeList.reduce((a, b)=>{return a + (b.indexOf('null')!=-1)}, 0); 39 | } 40 | 41 | /** 42 | * Used to make creating command schemas easier, since they're all very similar. 43 | * @param {string} name 44 | * @param {string[]} parameters 45 | */ 46 | function makeCommandSchema(name, parameters) { 47 | 48 | let schema = { 49 | "properties": { 50 | "name": { 51 | "type": "string", 52 | "pattern": `^${name}$`, 53 | }, 54 | "parameters": { 55 | "type": "array", 56 | "additionalItems": false, 57 | "minItems": 0, 58 | "maxItems": 0, 59 | }, 60 | }, 61 | "additionalProperties": false, 62 | "required": ["name", "parameters"], 63 | }; 64 | 65 | if (parameters.length) { 66 | schema.properties.parameters = { 67 | "type": "array", 68 | "items": parameters.map((s)=>{return {type: s}}), 69 | "additionalItems": false, 70 | "minItems": parameters.length - countNull(parameters), 71 | "maxItems": parameters.length, 72 | }; 73 | } 74 | 75 | return schema; 76 | } 77 | 78 | let doToAreaParams = Array(6).fill('integer').concat([['boolean', 'null']]).concat(Array(2).fill(['integer', 'null'])); 79 | let moveParams = Array(3).fill('integer').concat([['boolean', 'null'], ['integer', 'null']]); 80 | 81 | const commandSchemas = { 82 | scanArea: ['integer', ['integer', 'null']], 83 | viewInventory: [], 84 | equip: [], 85 | dig: doToAreaParams, 86 | place: doToAreaParams, 87 | move: moveParams, 88 | interact: moveParams, 89 | inspect: moveParams, 90 | select: ['integer'], 91 | transfer: Array(5).fill('integer'), 92 | craft: ['string'], 93 | raw: ['string'], 94 | sendPosition: [], 95 | sendComponents: [], 96 | config: [['string', 'null'], ['string', 'integer', 'boolean', 'null']], 97 | } 98 | 99 | const validators = {}; 100 | 101 | for (let id in commandSchemas) { 102 | let subSchema = makeCommandSchema(id, commandSchemas[id]); 103 | makeCommandValidator(ajv, subSchema, id, validators); 104 | } 105 | 106 | try { 107 | module.exports = validators; 108 | } 109 | catch (e) {;} -------------------------------------------------------------------------------- /public/lua/oc/config.lua: -------------------------------------------------------------------------------- 1 | local ser = require("serialization"); 2 | 3 | local configPath = "/home/lib/config.txt"; 4 | 5 | local promptMap = { 6 | robotName = "Enter a name for your robot.", 7 | accountName = "Enter your Roboserver account name.", 8 | serverIP = "Enter the IP address of your Roboserver.", 9 | serverPort = "Enter the port of your Roboserver.", 10 | tcpPort = "Enter the TCP port for your Roboserver.", 11 | posX = "Enter your robot's X coordinate.", 12 | posY = "Enter your robot's Y coordinate.", 13 | posZ = "Enter your robot's Z coordinate.", 14 | orient = "Enter 0 if your robot is facing South, 1 if East, 2 if North, 3 if West.", 15 | raw = "Enter true to allow raw Lua commands to run on this robot, false to ignore them.", 16 | }; 17 | 18 | function readFile(path) 19 | local file = io.open(path, "r"); 20 | if not file then return nil; end 21 | local content = file:read("*all"); 22 | file:close(); 23 | return content; 24 | end 25 | 26 | function getConfig(filePath) 27 | local config = {}; 28 | local content = readFile(filePath); 29 | if content then 30 | config = ser.unserialize(content); 31 | end 32 | return config; 33 | end 34 | 35 | function setConfig(configTable, filePath) 36 | local file = io.open(filePath, "w"); 37 | local configString = ser.serialize(configTable); 38 | file:write(configString); 39 | file:close(); 40 | return configString; 41 | end 42 | 43 | function setConfigOptions(options, path) 44 | local config = getConfig(path); 45 | for key, value in pairs(options) do 46 | config[key] = value; 47 | end 48 | return setConfig(config, path) 49 | end 50 | 51 | function readNotEmpty() 52 | local result = nil; 53 | local value = io.read(); 54 | if value ~= "" then result = value; end 55 | return result; 56 | end 57 | 58 | function readNewConfigOption(prompt, oldValue) 59 | print(prompt); 60 | if oldValue then 61 | print("Current value: " .. oldValue); 62 | end 63 | return readNotEmpty() or oldValue; 64 | end 65 | 66 | function readConfigOptions(options, path) 67 | local oldConfig = getConfig(path); 68 | print("Changing configuration. Just press enter to leave a value unchanged."); 69 | for i, property in pairs(options) do 70 | oldConfig[property] = readNewConfigOption(promptMap[property], oldConfig[property]); 71 | end 72 | return setConfig(oldConfig, path); 73 | end 74 | 75 | function easyConfig(path) 76 | local promptOrder = {"serverIP", "serverPort", "accountName", "robotName", "posX", "posY", "posZ", "orient"}; 77 | local result = readConfigOptions(promptOrder, path); 78 | setAvailableComponents(path); 79 | return result; 80 | end 81 | 82 | local arg = {...}; 83 | if arg[1] and arg[2] and not (arg[1] == 'config') then 84 | setConfigOptions({[arg[1]]=arg[2]}, "/home/lib/config.txt"); 85 | print('Set config option ' .. arg[1] .. ' to ' .. arg[2]); 86 | elseif not (arg[1] == 'config') then 87 | print('Usage: lua /home/lib/config.lua settingName settingValue'); 88 | end 89 | 90 | function setAvailableComponents(path) 91 | local availableComponents = {}; 92 | setConfigOptions({components=availableComponents}, path); 93 | end 94 | 95 | return { 96 | get = getConfig, 97 | set = setConfigOptions, 98 | easy = easyConfig, 99 | path = configPath, 100 | }; -------------------------------------------------------------------------------- /public/lua/oc/moveAndScan.lua: -------------------------------------------------------------------------------- 1 | local scan = require('scanDirection'); 2 | local orient = require('trackOrientation'); 3 | local pos = require('trackPosition'); 4 | local robot = require('robot'); 5 | 6 | local position = pos.get(); 7 | 8 | local M = {}; 9 | 10 | function doNothing() end 11 | 12 | local directionToNoScanMap = { 13 | ["forward"] = doNothing, 14 | ["back"] = doNothing, 15 | ["up"] = doNothing, 16 | ["down"] = doNothing, 17 | }; 18 | 19 | local directionToScanSmallMap = { 20 | ["forward"] = scan.forwardSmall, 21 | ["back"] = scan.backSmall, 22 | ["up"] = scan.upSmall, 23 | ["down"] = scan.downSmall, 24 | }; 25 | 26 | local directionToScanBigMap = { 27 | ["forward"] = function(times) for i=-1,7 do scan.forwardBig(0, times); end end, 28 | ["back"] = function(times) for i=-1,7 do scan.backBig(0, times); end end, 29 | ["up"] = scan.upBig, 30 | ["down"] = scan.downBig, 31 | }; 32 | 33 | local scanTypeMap = { 34 | [0] = directionToNoScanMap, 35 | [1] = directionToScanSmallMap, 36 | [2] = directionToScanBigMap, 37 | }; 38 | 39 | local directionToMoveFunctionMap = { 40 | ["forward"] = pos.forward, 41 | ["back"] = pos.back, 42 | ["up"] = pos.up, 43 | ["down"] = pos.down 44 | }; 45 | 46 | local directionToDetectFunctionMap = { 47 | ["forward"] = robot.detect, 48 | ["back"] = doNothing, 49 | ["up"] = robot.detectUp, 50 | ["down"] = robot.detectDown 51 | }; 52 | 53 | function M.moveAndScan(direction, scanType, times) 54 | local result = not directionToDetectFunctionMap[direction](); 55 | if result then 56 | result = directionToMoveFunctionMap[direction](); 57 | pos.save(); 58 | orient.save(); 59 | scanType = scanType or 0; 60 | scanTypeMap[scanType][direction](times); 61 | end 62 | return result; 63 | end 64 | 65 | -- try to reach the desired X or Z coordinate until it fails 66 | function M.approach(target, current, faceAxis, scanType, times) 67 | if target ~= current then 68 | local dist = target - current; 69 | faceAxis(dist); 70 | for i = 1, math.abs(dist) do 71 | if not M.moveAndScan('forward', scanType, times) then return false; end 72 | end 73 | end 74 | return true; 75 | end 76 | 77 | -- try to reach the desired Y coordinate until it fails 78 | function M.approachY(target, scanType, times) 79 | if target ~= position.y then 80 | 81 | local dist = target - position.y; 82 | 83 | local direction; 84 | if dist > 0 then 85 | direction = 'up'; 86 | else 87 | direction = 'down'; 88 | end 89 | 90 | for i = 1, math.abs(dist) do 91 | if not M.moveAndScan(direction, scanType, times) then return false; end 92 | end 93 | 94 | end 95 | return true; 96 | end 97 | 98 | -- attempt to go to coordinate until we get stuck 99 | function M.to(x, y, z, relative, scanType, times) 100 | if relative then 101 | x, y, z = pos.toAbsolute(x, y, z); 102 | end 103 | local start = { 104 | x = position.x, 105 | y = position.y, 106 | z = position.z, 107 | }; 108 | local xReached = M.approach(x, position.x, orient.faceX, scanType, times); 109 | local zReached = M.approach(z, position.z, orient.faceZ, scanType, times); 110 | local yReached = M.approachY(y, scanType, times); 111 | if xReached and zReached and yReached then 112 | return true; 113 | elseif 114 | start.x == position.x and 115 | start.y == position.y and 116 | start.z == position.z then 117 | return false; 118 | else 119 | return M.to(x, y, z, false, scanType, times); 120 | end 121 | end 122 | 123 | return M; 124 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f0f0f0; 3 | margin: 0px; 4 | overflow: hidden; 5 | font-family: monospace; 6 | } 7 | 8 | .message { 9 | font-size: 14px; 10 | display: inline-block; 11 | padding: 4px; 12 | border-radius: 25px; 13 | margin: 5px 0px; 14 | color: #FFFFFF; 15 | max-width: 100%; 16 | overflow-wrap: break-word; 17 | } 18 | 19 | .input { 20 | background-color: #337ab7; 21 | border: 1px solid #2e6da4; 22 | cursor: pointer; 23 | } 24 | .input:hover { 25 | background-color: #286090; 26 | border: 1px solid #204d74; 27 | } 28 | 29 | .output { 30 | background-color: #eeeeee; 31 | border: 1px solid #cccccc; 32 | color: #555555; 33 | } 34 | 35 | #bottomLeftUI { 36 | position: fixed; 37 | top: 99%; 38 | left: 1%; 39 | transform:translateY(-100%); 40 | } 41 | 42 | #bottomLeftUI > div { 43 | display: inline-block; 44 | } 45 | 46 | #messageContainer { 47 | overflow-y: scroll; 48 | width: 100%; 49 | border: 1px solid #cccccc; 50 | padding: 5px; 51 | } 52 | 53 | #messageContainer:empty { 54 | display: none; 55 | } 56 | 57 | #buttonContainer { 58 | position: absolute; 59 | right: 1%; 60 | top: 1%; 61 | width:270px; 62 | height: 100vh; 63 | } 64 | 65 | #topLeftUI { 66 | position: fixed; 67 | top: 1%; 68 | left: 1%; 69 | } 70 | 71 | .itemStackNumber { 72 | position: absolute; 73 | right: 0px; 74 | bottom: 0px; 75 | background: rgba(0, 0, 0, .5); 76 | border-radius: 50%; 77 | width: 18px; 78 | height: 18px; 79 | } 80 | 81 | .coordinateInput { 82 | max-width: 100%; 83 | text-align: center; 84 | border-width: 0px 1px 1px 0px; 85 | float: left; 86 | } 87 | 88 | .mc-table { 89 | display: inline-block; 90 | background-color: #c6c6c6; 91 | border: 2px solid; 92 | border-color: #dbdbdb #5b5b5b #5b5b5b #dbdbdb; 93 | padding: 0px 6px 6px 6px; 94 | margin: 5px; 95 | } 96 | 97 | .mc-td[data-selected=true] { 98 | border-color: lime; 99 | } 100 | .mc-td:hover { 101 | background-color: #C3C3C3; 102 | } 103 | 104 | .mc-td { 105 | color: white; 106 | display: inline-block; 107 | background-color: #8b8b8b; 108 | border: 2px solid; 109 | border-color: #373737 #ffffff #ffffff #373737; 110 | width: 40px; 111 | height: 40px; 112 | text-align: left; 113 | user-select: none; 114 | overflow: hidden; 115 | } 116 | 117 | .mc-td > div { 118 | cursor: pointer; 119 | width: 100%; 120 | height: 100%; 121 | position: relative; 122 | } 123 | 124 | .hidden { 125 | display: none; 126 | } 127 | 128 | .flex-row { 129 | display: flex; 130 | } 131 | 132 | .flex-col { 133 | display: flex; 134 | flex-direction: column; 135 | align-items: flex-start; 136 | } 137 | 138 | .flex-item { 139 | min-width: 0; 140 | min-height: 0; 141 | } 142 | 143 | .fullWidth { 144 | width: 100%; 145 | } 146 | 147 | .information-panel { 148 | background-color: #f5f5f5; 149 | } 150 | 151 | .input-group-select { 152 | /* rounded corner fix for osx chrome */ 153 | border: 0; 154 | outline: 1px solid #CCC; 155 | outline-offset: -1px; 156 | } 157 | 158 | .inline-block { 159 | display: inline-block; 160 | } 161 | 162 | /* bootstrap modifications */ 163 | 164 | .panel-default { 165 | border-color: #cccccc; 166 | } 167 | 168 | .panel-default > .panel-body { 169 | padding: 5px; 170 | } 171 | 172 | .panel-default > .panel-heading { 173 | padding: 5px; 174 | border-color: #cccccc; 175 | font-weight: bold; 176 | } 177 | 178 | .input-group-addon { 179 | background-color: #f5f5f5; 180 | font-weight: bold; 181 | padding: 5px; 182 | color: #333333; 183 | } -------------------------------------------------------------------------------- /routes/routes.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | var passport = require('passport'); 5 | 6 | var bcrypt = require('bcryptjs'); 7 | 8 | function loggedIn(req, res, next) { 9 | if (req.isAuthenticated()) {next();} 10 | else {res.redirect('/login');} 11 | } 12 | 13 | 14 | // main page where you enter commands 15 | router.get('/', loggedIn, function(req, res) { 16 | res.render('index', {user: req.user}); 17 | }); 18 | 19 | // login and registration page 20 | router.get('/login', function(req, res) { 21 | res.render('login.ejs', {error: false, active: 'login'}); 22 | }); 23 | 24 | router.get('/logout', function(req, res){ 25 | req.logout(); 26 | req.session.destroy(console.error); 27 | res.redirect('/login'); 28 | }); 29 | 30 | function makeLogInOrRedirect(req, res, next) { 31 | return (err, user, info)=>{ 32 | if (err) { return next(err); } 33 | if (!user) { return res.render('login.ejs', {error: 'Login failed.', active: 'login'}); } 34 | req.logIn(user, function(err) { 35 | if (err) { return next(err); } 36 | return res.redirect('/'); 37 | }); 38 | }; 39 | } 40 | 41 | router.post('/login', (req, res, next)=>{ 42 | passport.authenticate('local', makeLogInOrRedirect(req, res, next))(req, res, next); 43 | }); 44 | 45 | const saltRounds = 10; 46 | router.post('/register', (req, res, next)=>{ 47 | var db = req.app.get('db'); 48 | bcrypt.hash(req.body.password, saltRounds).then((hash)=>{ 49 | db.findOne({username: req.body.username}).then((doc)=>{ 50 | console.log(req.body.username, doc) 51 | if (doc) { 52 | return res.render('login.ejs', {error: 'Username unavailable.', active: 'register'}); 53 | } 54 | else { 55 | db.insert({username: req.body.username, passwordHash: hash}).then((newDoc)=>{ 56 | passport.authenticate('local', makeLogInOrRedirect(req, res, next))(req, res, next); 57 | }) 58 | .catch((err)=>{return next(err);}); 59 | } 60 | }) 61 | .catch((err)=>{return next(err);}); 62 | }) 63 | .catch((err)=>{return next(err);}); 64 | }); 65 | 66 | // allows robots to look up crafting recipes 67 | var minecraftRecipes = require('../public/js/recipes/minecraftRecipes.json'); 68 | var OCRecipes = require('../public/js/recipes/OCRecipes.json'); 69 | var allRecipes = minecraftRecipes.concat(OCRecipes); 70 | var recipeSearch = require('../public/js/shared/recipeSearch.js'); 71 | 72 | router.get('/recipe/:recipeName', function(req, res) { 73 | var recipeName = req.params.recipeName; 74 | var recipes = recipeSearch.findRecipeFor(recipeName, allRecipes); 75 | var productRecipes = recipes.map((recipe)=>{return recipeSearch.extractRecipeFor(recipeName, recipe);}); 76 | res.send(productRecipes); 77 | }); 78 | 79 | // allows robots to look up block hardness values 80 | let minecraftData = require('minecraft-data')('1.12.2'); 81 | let namesToHardness = {}; 82 | for (let block of minecraftData.blocksArray) { 83 | namesToHardness[block.name] = block.hardness; 84 | } 85 | 86 | router.get('/namesToHardness', function(req, res) { 87 | res.send(namesToHardness); 88 | }); 89 | 90 | router.get('/blockData/:blockName', function(req, res) { 91 | let blockName = req.params.blockName; 92 | let blockData = minecraftData.findItemOrBlockByName(blockName); 93 | res.send(blockData); 94 | }); 95 | 96 | // let the web client see what version we're using 97 | let version = require('../package').version; 98 | router.get('/version', function(req, res) { 99 | res.send(version); 100 | }); 101 | 102 | module.exports = router; 103 | -------------------------------------------------------------------------------- /public/lua/oc/commandMap.lua: -------------------------------------------------------------------------------- 1 | local sendScan = require('sendScan'); 2 | local int = require('interact'); 3 | local component = require('component'); 4 | local inv = component.inventory_controller; 5 | local robot = require('robot'); 6 | local dta = require('doToArea'); 7 | local mas = require('moveAndScan'); 8 | local craft = require('craft'); 9 | local pos = require('trackPosition'); 10 | 11 | tcp = require('tcp'); -- if this is local, reloading modules fails in commandLoop 12 | local config = require('config'); 13 | 14 | local M = {}; 15 | 16 | M['scanArea'] = function(scanLevel, times) 17 | local result; 18 | if scanLevel == 1 then 19 | for i=-2,5 do 20 | result = sendScan.volume(-3, -3, i, 8, 8, 1, times) 21 | end 22 | elseif scanLevel == 2 then 23 | for i=-1,7 do 24 | result = sendScan.plane(i, times); 25 | end 26 | end 27 | return result; 28 | end; 29 | 30 | M['viewInventory'] = function() 31 | return int.sendInventoryData(-1); 32 | end; 33 | 34 | M['equip'] = function() 35 | inv.equip(); 36 | int.sendInventoryMetadata(-1); 37 | return int.sendSlotData(-1, robot.select()); 38 | end; 39 | 40 | M['dig'] = function(x1, y1, z1, x2, y2, z2, relative, scanLevel, selectionIndex) 41 | return dta.digArea(x1, y1, z1, x2, y2, z2, relative, scanLevel, selectionIndex); 42 | end; 43 | 44 | M['place'] = function(x1, y1, z1, x2, y2, z2, relative, scanLevel, selectionIndex) 45 | return dta.placeArea(x1, y1, z1, x2, y2, z2, relative, scanLevel, selectionIndex); 46 | end; 47 | 48 | M['move'] = function(x, y, z, relative, scanLevel) 49 | return mas.to(x, y, z, relative, scanLevel); 50 | end; 51 | 52 | M['interact'] = function(x, y, z, relative, scanLevel) 53 | return int.interact(x, y, z, relative, scanLevel); 54 | end; 55 | 56 | M['inspect'] = function(x, y, z, relative, scanLevel) 57 | return int.inspect(x, y, z, relative, scanLevel); 58 | end; 59 | 60 | M['select'] = function(slotNum) 61 | return robot.select(slotNum); 62 | end; 63 | 64 | M['transfer'] = function(fromSlot, fromSide, toSlot, toSide, amount) 65 | return int.transfer(fromSlot, fromSide, toSlot, toSide, amount); 66 | end; 67 | 68 | M['craft'] = function(itemName) 69 | return craft.craft(itemName); 70 | end; 71 | 72 | function runInTerminal(commandText) 73 | local file = assert(io.popen(commandText, 'r')); 74 | local output = file:read('*all'); 75 | file:close(); 76 | return output; 77 | end 78 | 79 | M['raw'] = function(commandString) 80 | local raw = config.get(config.path).raw; 81 | local rawBool = (raw == "true" or raw == true) and true or false; 82 | local result; 83 | if rawBool then 84 | local status; 85 | local command = load(commandString, nil, 't', _ENV); 86 | status, result = pcall(command); 87 | else 88 | result = false; 89 | end 90 | return result; 91 | end; 92 | 93 | M['sendPosition'] = function() 94 | return pos.sendLocation(); 95 | end; 96 | 97 | M['sendComponents'] = function() 98 | return tcp.write({['available components']=component.list()}); 99 | end; 100 | 101 | M['config'] = function(optionName, optionValue) 102 | local result; 103 | if optionName and (optionValue ~= nil) then 104 | config.set({[optionName]=optionValue}, config.path); 105 | result = true; 106 | elseif optionName then 107 | local options = {}; 108 | options[optionName] = config.get(config.path)[optionName]; 109 | result = tcp.write({['config']=options}); 110 | else 111 | result = tcp.write({['config']=config.get(config.path)}); 112 | end 113 | return result; 114 | end; 115 | 116 | M['message'] = function(message) 117 | return print(message); 118 | end; 119 | 120 | return M; -------------------------------------------------------------------------------- /public/js/shared/InventoryData.js: -------------------------------------------------------------------------------- 1 | let validators = require('./fromRobotSchemas.js').validators; 2 | /** 3 | * Used to simulate an in-game inventory for the test client 4 | * and to represent the in-game inventory on the web client. 5 | */ 6 | class InventoryData { 7 | 8 | /** 9 | * Used to set the initial state of a simulated inventory from test data. 10 | * @param {object} inventoryMeta 11 | */ 12 | constructor(inventoryMeta) { 13 | validators.inventoryMeta(inventoryMeta); 14 | this.size = inventoryMeta.size; 15 | this.side = inventoryMeta.side; 16 | this.selected = inventoryMeta.selected; 17 | this.slots = {}; 18 | } 19 | 20 | /** 21 | * Used to change the contents of a slot in the inventory. 22 | * @param {object} inventorySlot 23 | */ 24 | setSlot(inventorySlot) { 25 | validators.inventorySlot(inventorySlot); 26 | this.slots[inventorySlot.slotNum] = inventorySlot.contents; 27 | } 28 | 29 | /** 30 | * Used to format slot data in a way the server understands. 31 | * @param {number} slotNum 32 | * @returns {object} 33 | */ 34 | serializeSlot(slotNum) { 35 | let inventorySlot = { 36 | side: this.side, 37 | slotNum: slotNum, 38 | contents: this.slots[slotNum], 39 | }; 40 | return inventorySlot; 41 | } 42 | 43 | /** 44 | * Used to determine whether items can stack. 45 | * Doesn't take into account remaining space in the stacks. 46 | * @param {object} itemStack1 47 | * @param {object} itemStack2 48 | */ 49 | canStack(itemStack1, itemStack2) { 50 | let sameName = itemStack1.name == itemStack2.name; 51 | let noDamage = !itemStack1.damage && !itemStack2.damage; 52 | let noTag = !itemStack1.hasTag && !itemStack2.hasTag; 53 | let result = false; 54 | if (sameName && noDamage && noTag) { 55 | result = true; 56 | } 57 | return result; 58 | } 59 | 60 | /** 61 | * Used to make sure a transfer obeys inventory rules before we execute it. 62 | * @param {object} fromSlot 63 | * @param {object} toSlot 64 | * @param {number} desiredTransferAmount 65 | * @return {number} 66 | */ 67 | validateTransfer(fromSlot, toSlot, desiredTransferAmount) { 68 | if (fromSlot.contents) {validators.inventorySlot(fromSlot);} 69 | if (toSlot.contents) {validators.inventorySlot(toSlot);} 70 | 71 | let finalTransferAmount = 0; 72 | 73 | if (!fromSlot.contents || fromSlot.side !== -1 && toSlot.side !== -1) {;} 74 | else { 75 | let fromItemStack = fromSlot.contents; 76 | if (desiredTransferAmount > fromItemStack.size || desiredTransferAmount < 1) {;} 77 | else if (!toSlot.contents) { 78 | finalTransferAmount = desiredTransferAmount || fromItemStack.size; 79 | } 80 | else { 81 | let toItemStack = toSlot.contents; 82 | if (this.canStack(fromItemStack, toItemStack)) { 83 | var toItemStackSpace = toItemStack.maxSize - toItemStack.size; 84 | if (toItemStackSpace < 1) {;} 85 | else { 86 | let actualTransferAmount; 87 | if (desiredTransferAmount) { 88 | actualTransferAmount = Math.min(desiredTransferAmount, toItemStackSpace); 89 | } 90 | else { 91 | actualTransferAmount = Math.min(fromItemStack.size, toItemStackSpace); 92 | } 93 | finalTransferAmount = actualTransferAmount; 94 | } 95 | } 96 | else { 97 | if (!desiredTransferAmount || desiredTransferAmount == fromItemStack.size) { 98 | finalTransferAmount = fromItemStack.size; 99 | } 100 | } 101 | } 102 | } 103 | return finalTransferAmount; 104 | } 105 | 106 | } 107 | 108 | try {module.exports = InventoryData;} 109 | catch(e) {;} -------------------------------------------------------------------------------- /public/js/client/Robot.mjs: -------------------------------------------------------------------------------- 1 | import {WorldAndScenePoint} from '/js/client/WorldAndScenePoint.mjs'; 2 | 3 | /** 4 | * An organized collection of all important data about a connected robot. 5 | */ 6 | export class Robot { 7 | 8 | /** 9 | * An organized collection of all important data about a connected robot. 10 | */ 11 | constructor() { 12 | this.showInventories = false; 13 | this.inventories = {}; 14 | this.components = []; 15 | } 16 | 17 | /** 18 | * Used to display how much power remains in the GUI. 19 | * @returns {number} 20 | */ 21 | getPower() { 22 | return this.power; 23 | } 24 | 25 | /** 26 | * Receive an updated power value from the robot. 27 | * @param {number} power 28 | */ 29 | setPower(power) { 30 | this.power = power; 31 | } 32 | 33 | /** 34 | * Used to know where to look when this robot is selected, where to put the select highlight, 35 | * and to make sure the robot material isn't overwritten to the stone material by terrain scans. 36 | * @returns {WorldAndScenePoint} 37 | */ 38 | getPosition() { 39 | return this.position; 40 | } 41 | 42 | /** 43 | * Receive an updated location from the robot. 44 | * @param {WorldAndScenePoint} point 45 | */ 46 | setPosition(point) { 47 | this.position = point; 48 | } 49 | 50 | /** 51 | * Needed so we can call methods of the robot's inventories. 52 | * @param {number} side 53 | * @returns {Inventory} 54 | */ 55 | getInventory(side) { 56 | return this.inventories[side]; 57 | } 58 | 59 | /** 60 | * Receive an updated inventory from the robot. Will overwrite existing inventories from the same side. 61 | * @param {number} side 62 | * @param {Inventory} inventory 63 | */ 64 | addInventory(inventory) { 65 | var oldInventory = this.inventories[inventory.inventory.side]; 66 | if (oldInventory) {oldInventory.removeFromDisplay();} 67 | this.inventories[inventory.inventory.side] = inventory; 68 | } 69 | 70 | /** 71 | * Used when we want to toggle the visibility of all a robot's inventories. 72 | * @returns {Inventory[]} 73 | */ 74 | getAllInventories() { 75 | return Object.values(this.inventories); 76 | } 77 | 78 | /** 79 | * Used when we want to remove all a robot's external inventories when it moves. 80 | * @returns {Inventory[]} 81 | */ 82 | getAllExternalInventories() { 83 | return Object.keys(this.inventories) 84 | .filter(side => side != -1) 85 | .map(side => this.inventories[side]); 86 | } 87 | 88 | /** 89 | * Used when we want to remove all a robot's external inventories when it moves. 90 | */ 91 | removeAllExternalInventories() { 92 | var internalInventories = {}; 93 | for (var inventory of Object.values(this.inventories)) { 94 | var side = inventory.getSide() 95 | if (side == -1) {internalInventories[side] = inventory;} 96 | else {inventory.removeFromDisplay();} 97 | } 98 | this.inventories = internalInventories; 99 | } 100 | 101 | /** 102 | * The robot's available components are used to customize the GUI to each robot's abilities. 103 | */ 104 | getComponents() { 105 | return this.components; 106 | } 107 | 108 | /** 109 | * The robot's available components are set when it connects. 110 | * @param {string[]} components 111 | */ 112 | setComponents(components) { 113 | this.components = components; 114 | } 115 | 116 | /** 117 | * Used to tell the UI whether this robot's inventories should be displayed or not. 118 | * @returns {boolean} 119 | */ 120 | getShowInventories() { 121 | return this.showInventories; 122 | } 123 | 124 | /** 125 | * Used when the inventory button is pressed. The UI reads this and changes accordingly. 126 | */ 127 | toggleShowInventories() { 128 | this.showInventories = !this.showInventories; 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /public/lua/oc/doToArea.lua: -------------------------------------------------------------------------------- 1 | local adj = require('adjacent'); 2 | local robot = require('robot'); 3 | tcp = require('tcp'); -- if this is local, reloading modules fails in commandLoop 4 | local pos = require('trackPosition'); 5 | local component = require('component'); 6 | if not component.isAvailable('geolyzer') then 7 | error('Geolyzer not found'); 8 | end 9 | local geolyzer = component.geolyzer; 10 | local int = require('interact'); 11 | 12 | local M = {}; 13 | 14 | function M.getMinPoint(p1, p2) 15 | local minPoint = {x=1e100, y=1e100, z=1e100}; 16 | for axis in pairs(p1) do 17 | minPoint[axis] = math.min(minPoint[axis], p1[axis], p2[axis]) 18 | end 19 | return minPoint; 20 | end 21 | 22 | function M.getMaxPoint(p1, p2) 23 | local maxPoint = {x=-1e100, y=-1e100, z=-1e100}; 24 | for axis in pairs(p1) do 25 | maxPoint[axis] = math.max(maxPoint[axis], p1[axis], p2[axis]) 26 | end 27 | return maxPoint; 28 | end 29 | 30 | function M.generateBoxPoints(corner1, corner2) 31 | local minPoint = M.getMinPoint(corner1, corner2); 32 | local maxPoint = M.getMaxPoint(corner1, corner2); 33 | local points = {}; 34 | for x = minPoint.x, maxPoint.x do 35 | for y = minPoint.y, maxPoint.y do 36 | for z = minPoint.z, maxPoint.z do 37 | table.insert(points, {x=x,y=y,z=z}); 38 | end 39 | end 40 | end 41 | return points; 42 | end 43 | 44 | function M.doToAllPoints(pointList, action) 45 | local success = true; 46 | for i = 1, #pointList do 47 | success = action(pointList[i]) and success; 48 | end 49 | return success; 50 | end 51 | 52 | function M.makeApproachAndDoAction(action, scanType, times) 53 | return function (point) 54 | local moveSuccess = adj.toAdjacent(point, scanType, times); 55 | local actionSuccess = false; 56 | if moveSuccess then 57 | actionSuccess = action(point); 58 | end 59 | return moveSuccess and actionSuccess; 60 | end 61 | end 62 | 63 | function M.makeDoActionToArea(action) 64 | return function (x1, y1, z1, x2, y2, z2, relative, scanType, index, times) 65 | if relative then 66 | x1, y1, z1 = pos.toAbsolute(x1, y1, z1); 67 | x2, y2, z2 = pos.toAbsolute(x2, y2, z2); 68 | end 69 | local p1 = {x=x1, y=y1, z=z1}; 70 | local p2 = {x=x2, y=y2, z=z2}; 71 | local pointList = M.generateBoxPoints(p1, p2); 72 | adj.distanceSort(pos.get(), pointList); 73 | local approachAndDoAction = M.makeApproachAndDoAction(action, scanType, times); 74 | local actionSuccess = M.doToAllPoints(pointList, approachAndDoAction); 75 | if index then -- 0 is true 76 | tcp.write({['delete selection']=index}); 77 | end 78 | return actionSuccess; 79 | end 80 | end 81 | 82 | function M.dig(point) 83 | local robotPos = pos.get(); 84 | local pointSide = 3; -- front 85 | if point.y > robotPos.y then 86 | pointSide = 1; -- top 87 | elseif point.y < robotPos.y then 88 | pointSide = 0; -- bottom 89 | end 90 | local swingSuccess = true; 91 | if component.robot.detect(pointSide) then 92 | swingSuccess = component.robot.swing(pointSide); 93 | if swingSuccess then 94 | tcp.write({['dig success']=point}); 95 | end 96 | end 97 | return swingSuccess; 98 | end 99 | 100 | M.digArea = M.makeDoActionToArea(M.dig); 101 | 102 | function M.place(point) 103 | local robotPos = pos.get(); 104 | local pointSide = 3; -- front 105 | if point.y > robotPos.y then 106 | pointSide = 1; -- top 107 | elseif point.y < robotPos.y then 108 | pointSide = 0; -- bottom 109 | end 110 | local placeSuccess = component.robot.place(pointSide); 111 | if placeSuccess then 112 | local blockData = geolyzer.analyze(pointSide); 113 | blockData.point = point; 114 | tcp.write({['block data']=blockData}); 115 | int.sendSlotData(-1, robot.select()); 116 | end 117 | return placeSuccess; 118 | end 119 | 120 | M.placeArea = M.makeDoActionToArea(M.place); 121 | 122 | return M; -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 25 |
26 | 27 |
28 | 29 |
active<% } %>" id="loginTab"> 30 |
31 |
32 |
33 | <% if (error && (active == "login")) { %><%= error %><% } %> 34 |
35 | 36 |
37 | 38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 | 61 |
active<% } %>" id="registerTab"> 62 |
63 |
64 |
65 | <% if (error && (active == "register")) { %><%= error %><% } %> 66 |
67 | 68 |
69 | 70 |
71 |
72 | 73 |
74 | 75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 |
86 |
87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /public/lua/oc/interact.lua: -------------------------------------------------------------------------------- 1 | local component = require('component'); 2 | if not component.isAvailable('inventory_controller') then 3 | error('Inventory controller not found'); 4 | end 5 | local inv = component.inventory_controller; 6 | if not component.isAvailable('geolyzer') then 7 | error('Geolyzer not found'); 8 | end 9 | local geolyzer = component.geolyzer; 10 | tcp = require('tcp'); -- if this is local, reloading modules fails in commandLoop 11 | local robot = require('robot'); 12 | local adj = require('adjacent'); 13 | local pos = require('trackPosition'); 14 | 15 | local M = {}; 16 | 17 | function M.sendSlotData(side, slotNum) 18 | local slot = { 19 | side = side, 20 | slotNum = slotNum 21 | }; 22 | if side == -1 then 23 | if robot.count(slotNum) > 0 then 24 | slot.contents = inv.getStackInInternalSlot(slotNum); 25 | end 26 | else 27 | slot.contents = inv.getStackInSlot(side, slotNum); 28 | end 29 | tcp.write({['slot data']=slot}); 30 | return slot.contents; 31 | end 32 | 33 | function M.sendInventoryData(side) 34 | local size = M.sendInventoryMetadata(side); 35 | if size then 36 | for i = 1, size do 37 | M.sendSlotData(side, i); 38 | end 39 | end 40 | return size; 41 | end 42 | 43 | function M.sendInventoryMetadata(side) 44 | local inventory = {side = side}; 45 | if side == -1 then 46 | inventory.size = robot.inventorySize(); 47 | inventory.selected = robot.select(); 48 | else 49 | inventory.size = inv.getInventorySize(side); 50 | end 51 | if inventory.size then 52 | tcp.write({['inventory data']=inventory}); 53 | end 54 | return inventory.size; 55 | end 56 | 57 | function M.transfer(slot1, side1, slot2, side2, amount) 58 | local originalSlot = robot.select(); 59 | local success = false; 60 | if (side1 == -1 and side2 == -1) then 61 | robot.select(slot1); 62 | success = robot.transferTo(slot2, amount); 63 | elseif (side1 == -1) then 64 | robot.select(slot1); 65 | success = inv.dropIntoSlot(side2, slot2, amount); 66 | elseif (side2 == -1) then 67 | robot.select(slot2); 68 | success = inv.suckFromSlot(side1, slot1, amount); 69 | else 70 | error('Cannot transfer from one external inventory to another'); 71 | end 72 | robot.select(originalSlot); 73 | M.sendSlotData(side1, slot1); 74 | M.sendSlotData(side2, slot2); 75 | return success; 76 | end 77 | 78 | function M.interact(x, y, z, relative, scanType, times) 79 | if relative then 80 | x, y, z = pos.toAbsolute(x, y, z); 81 | end 82 | local point = {x=x, y=y, z=z}; 83 | local moveSuccess = adj.toAdjacent(point, scanType, times); 84 | local interactSuccess = false; 85 | if moveSuccess then 86 | local pointSide = 3; -- front 87 | local robotPos = pos.get(); 88 | if point.y > robotPos.y then 89 | pointSide = 1; -- top 90 | elseif point.y < robotPos.y then 91 | pointSide = 0; -- bottom 92 | end 93 | interactSuccess = M.sendInventoryData(pointSide); 94 | if not interactSuccess then 95 | interactSuccess = component.robot.use(pointSide); 96 | end 97 | end 98 | return moveSuccess and interactSuccess; 99 | end 100 | 101 | function M.inspect(x, y, z, relative, scanType, times) 102 | if relative then 103 | x, y, z = pos.toAbsolute(x, y, z); 104 | end 105 | local point = {x=x, y=y, z=z}; 106 | local moveSuccess = adj.toAdjacent(point, scanType, times); 107 | local inspectSuccess = false; 108 | if moveSuccess then 109 | local pointSide = 3; -- front 110 | local robotPos = pos.get(); 111 | if point.y > robotPos.y then 112 | pointSide = 1; -- top 113 | elseif point.y < robotPos.y then 114 | pointSide = 0; -- bottom 115 | end 116 | inspectSuccess = geolyzer.analyze(pointSide); 117 | inspectSuccess.point = point; 118 | tcp.write({['block data']=inspectSuccess}); 119 | end 120 | local inspectResult = false; 121 | if moveSuccess and inspectSuccess then 122 | inspectResult = inspectSuccess.name; 123 | end 124 | return inspectResult; 125 | end 126 | 127 | return M; -------------------------------------------------------------------------------- /public/js/lib/PointerLockControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used to let the camera fly around the scene. 3 | */ 4 | class PointerLockControls { 5 | 6 | /** 7 | * 8 | * @param {THREE.PerspectiveCamera} camera 9 | */ 10 | constructor ( camera ) { 11 | 12 | var scope = this; 13 | 14 | var pitchObject = new THREE.Object3D(); 15 | pitchObject.add( camera ); 16 | 17 | var yawObject = new THREE.Object3D(); 18 | yawObject.position.y = 10; 19 | yawObject.add( pitchObject ); 20 | 21 | var moveForward = false; 22 | var moveBackward = false; 23 | var moveLeft = false; 24 | var moveRight = false; 25 | var moveDown = false; 26 | var moveUp = false; 27 | 28 | var isOnObject = false; 29 | 30 | var velocity = new THREE.Vector3(); 31 | 32 | var PI_2 = Math.PI / 2; 33 | 34 | var onMouseMove = function ( event ) { 35 | 36 | if ( scope.enabled === false ) return; 37 | 38 | var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 39 | var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 40 | 41 | // fixes a bug causing the camera to rotate when the window is resized occasionally 42 | // may 14 2017 43 | if (Math.abs(movementX) < 300 && Math.abs(movementY) < 300) { 44 | yawObject.rotation.y -= movementX * 0.002; 45 | pitchObject.rotation.x -= movementY * 0.002; 46 | } 47 | 48 | pitchObject.rotation.x = Math.max( - PI_2, Math.min( PI_2, pitchObject.rotation.x ) ); 49 | 50 | }; 51 | 52 | var onKeyDown = function ( event ) { 53 | 54 | switch ( event.keyCode ) { 55 | 56 | case 38: // up 57 | case 87: // w 58 | moveForward = true; 59 | break; 60 | 61 | case 37: // left 62 | case 65: // a 63 | moveLeft = true; 64 | break; 65 | 66 | case 40: // down 67 | case 83: // s 68 | moveBackward = true; 69 | break; 70 | 71 | case 39: // right 72 | case 68: // d 73 | moveRight = true; 74 | break; 75 | 76 | 77 | case 16: // shift 78 | moveDown = true; 79 | break; 80 | 81 | 82 | case 32: // space 83 | moveUp = true; 84 | break; 85 | 86 | } 87 | 88 | }; 89 | 90 | var onKeyUp = function ( event ) { 91 | 92 | switch( event.keyCode ) { 93 | 94 | case 38: // up 95 | case 87: // w 96 | moveForward = false; 97 | break; 98 | 99 | case 37: // left 100 | case 65: // a 101 | moveLeft = false; 102 | break; 103 | 104 | case 40: // down 105 | case 83: // a 106 | moveBackward = false; 107 | break; 108 | 109 | case 39: // right 110 | case 68: // d 111 | moveRight = false; 112 | break; 113 | 114 | case 16: // shift 115 | moveDown = false; 116 | break; 117 | 118 | case 32: // space 119 | moveUp = false; 120 | break; 121 | 122 | } 123 | 124 | }; 125 | 126 | document.addEventListener( 'mousemove', onMouseMove, {capture: false, passive: false} ); 127 | document.addEventListener( 'keydown', onKeyDown, false ); 128 | document.addEventListener( 'keyup', onKeyUp, false ); 129 | 130 | this.enabled = false; 131 | 132 | this.getObject = function () { 133 | 134 | return yawObject; 135 | 136 | }; 137 | 138 | this.update = function ( delta ) { 139 | 140 | if ( scope.enabled === false ) return; 141 | 142 | delta *= 0.1; 143 | 144 | velocity.x += ( - velocity.x ) * 0.08 * delta; 145 | velocity.z += ( - velocity.z ) * 0.08 * delta; 146 | velocity.y += ( - velocity.y ) * 0.08 * delta; 147 | 148 | var speed = 2; 149 | 150 | if ( moveForward ) velocity.z -= speed * delta; 151 | if ( moveBackward ) velocity.z += speed * delta; 152 | 153 | if ( moveLeft ) velocity.x -= speed * delta; 154 | if ( moveRight ) velocity.x += speed * delta; 155 | 156 | if ( moveDown ) velocity.y -= speed * delta; 157 | if ( moveUp ) velocity.y += speed * delta; 158 | 159 | yawObject.translateX( velocity.x ); 160 | yawObject.translateY( velocity.y ); 161 | yawObject.translateZ( velocity.z ); 162 | 163 | }; 164 | 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var path = require('path'); 8 | 9 | var routes = require('./routes/routes'); 10 | 11 | var passport = require('passport'); 12 | var LocalStrategy = require('passport-local').Strategy; 13 | 14 | var Datastore = require('nedb-promise'); 15 | var db = new Datastore({ filename: path.join(__dirname, 'users.db'), autoload: true }); 16 | 17 | try { 18 | var config = require('./public/js/config/config'); 19 | } 20 | catch (err) { 21 | console.error('***'); 22 | console.error('no config file found, please rename public/js/config/config.example.js to config.js or create your own!'); 23 | console.error('***'); 24 | console.error(); 25 | process.exit(1); 26 | } 27 | 28 | var bcrypt = require('bcryptjs'); 29 | 30 | var app = express(); 31 | 32 | app.set('db', db); 33 | 34 | // view engine setup 35 | app.set('views', path.join(__dirname, 'views')); 36 | app.set('view engine', 'ejs'); 37 | 38 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 39 | app.use(logger('dev')); 40 | 41 | // i really doubt i need all three of these, fix later 42 | app.use(bodyParser.json({limit: '50mb'})); 43 | app.use(bodyParser.urlencoded({ extended: false, limit: '50mb' })); 44 | 45 | app.use(cookieParser()); 46 | app.use(express.static(path.join(__dirname, 'public'))); 47 | 48 | var session = require('express-session'); 49 | 50 | var NedbStore = require('nedb-session-store')(session); 51 | app.set('sessionStore', new NedbStore({filename: path.join(__dirname, 'sessions.db')})); 52 | 53 | // required for passport session 54 | app.use(session({ 55 | secret: config.expressSessionSecret, 56 | resave: false, 57 | saveUninitialized: false, 58 | store: app.get('sessionStore') 59 | })); 60 | 61 | app.use(passport.initialize()); 62 | app.use(passport.session()); 63 | 64 | passport.serializeUser(function(user, done) { 65 | done(null, user.username); 66 | }); 67 | 68 | passport.deserializeUser(function(username, done) { 69 | 70 | db.findOne({ username: username }).then((user)=>{ 71 | if (!user) { 72 | return done(null, false, { message: 'User not found.' }); 73 | } 74 | return done(null, {username: user.username}); 75 | }) 76 | .catch((err)=>{ 77 | return done(err); 78 | }); 79 | 80 | }); 81 | 82 | passport.use(new LocalStrategy(function(username, password, done) { 83 | process.nextTick(function() { 84 | 85 | db.findOne({ username: username }).then((user)=>{ 86 | 87 | if (!user) { 88 | return done(null, false, { message: 'Incorrect username.' }); 89 | } 90 | 91 | bcrypt.compare(password, user.passwordHash) 92 | .then((res)=>{ 93 | if (res) { 94 | return done(null, user); 95 | } 96 | else { 97 | return done(null, false, { message: 'Incorrect password.' }); 98 | } 99 | }) 100 | .catch((err)=>{ 101 | console.dir(err); 102 | return done(null, false, { message: 'An error occurred.' }); 103 | }); 104 | 105 | }) 106 | .catch((err)=>{ 107 | console.dir(err); 108 | return done(err); 109 | }); 110 | 111 | }); 112 | })); 113 | 114 | app.use('/', routes); 115 | 116 | // catch 404 and forward to error handler 117 | app.use(function(req, res, next) { 118 | var err = new Error('Not Found'); 119 | err.status = 404; 120 | next(err); 121 | }); 122 | 123 | // error handlers 124 | 125 | // development error handler 126 | // will print stacktrace 127 | if (app.get('env') === 'development') { 128 | app.use(function(err, req, res, next) { 129 | res.status(err.status || 500); 130 | res.render('error', { 131 | message: err.message, 132 | error: err 133 | }); 134 | }); 135 | } 136 | 137 | // production error handler 138 | // no stacktraces leaked to user 139 | app.use(function(err, req, res, next) { 140 | res.status(err.status || 500); 141 | res.render('error', { 142 | message: err.message, 143 | error: err 144 | }); 145 | }); 146 | 147 | module.exports = app; -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | * remember command 2 | * remember coordinates \[relative?\] \[name\] \[amount\] 3 | * store all names as lowercase 4 | * no name means inspect the block at the coords and remember that 5 | * unless it has an inventory, then remember the items? 6 | * doesn't work well for furnaces 7 | * no amount means it's a block 8 | * negative amounts mean subtract that many from what you remember 9 | * tells the server where items or blocks are 10 | * server should know the difference between items and blocks 11 | * server should know how many of an item are at a location 12 | 13 | * locate command 14 | * locate name \[amount\] 15 | * store all names as lowercase 16 | * no amount means it's a block 17 | * check inventory first, then ask server 18 | * server replies with coordinates of an inventory or block 19 | * check location provided by server, update it, ask again if invalid 20 | * if the server doesn't know, craft it 21 | 22 | * gather command, dig blocks matching gather list 23 | * just use equipped tool first 24 | * minecraft-data seems to provide harvestTools for blocks though 25 | 26 | * build by specifying block name rather than using current slot 27 | * new parameters or a new command? 28 | 29 | * add wander to movement for when pathing fails 30 | 31 | * setting to allow destroying while moving, towering 32 | * list of blocks it's okay to destroy and tower with 33 | 34 | # later 35 | * fix test client scanArea, others 36 | * they shouldn't always be sending command results 37 | * firefox 38 | * fix can't click inventory button 39 | * fix search catches wasd 40 | * make electron skip login page 41 | * make electron server disconnect users from normal browsers 42 | * once we're confident electron works, change install scripts to download from release tag 43 | 44 | ## big 45 | * blueprint storage and rendering 46 | * mapping: 47 | * Set up database 48 | * persistent server side maps that robots can read 49 | * dimension selector to handle robots in different dimensions or worlds 50 | * while we're changing the ui, put scan size selector next to scan button 51 | * detect maximum scan batch size based on available memory 52 | * don't add to the scene any voxels which are surrounded? 53 | * merge and split voxel meshes based on distance from robot 54 | * sort of like how minecraft loads chunks, hopefully this approach would improve rendering speed 55 | * impossible to retain individual coloring? 56 | * ocglasses roboserver client 57 | * highlight the block we're looking at 58 | * implement move command on punch first 59 | * create a way to select a tool in the overlay 60 | * implement the other tools 61 | * display contents of inventories when looking at them 62 | * use map data for x-ray vision? 63 | * visual tool for chaining robot commands together to automate tasks 64 | * first make an action recording feature 65 | * specify number of loops, duration of sleep between 66 | 67 | ## small 68 | * validate test data 69 | * rework to use https://github.com/PrismarineJS/minecraft-data 70 | * add a go away forever button to the message banner 71 | * change gui based on what components the selected robot has 72 | * modularize lua code so more of the requires/components are optional 73 | * allow limited functionality without crafting component 74 | * allow limited functionality without inventory controller 75 | * allow limited functionality without geolyzer 76 | * customizable crafting recipes 77 | * all known inventories tracked per account, robots check those first when crafting or building 78 | * virtual items/blocks? 79 | * perform initial configuration from application? 80 | * hotkeys for different tools 81 | * find blocks feature (make non-matches mostly transparent) 82 | * current order of acting on an area of blocks not the most efficient 83 | * maybe do a column at a time but sort those by distance 84 | * alternatively sort all the points by distance after each action 85 | * split command history by robot 86 | * display most recently equipped item 87 | * turn hardness values into hardness classes based on data accuracy 88 | * i.e., say a block could be either hardness 2 or 1.5 89 | * show hardness classes for the block the cursor is on 90 | * highlight blocks of a specific hardness class 91 | * highlight neighbors of a block that fit in the same hardness class 92 | * ability to change key bindings 93 | * item label to facade color map, use for map and inventory 94 | * see if we can make water source and flow blocks render differently 95 | * render robot facing (triangular prism or different colored face) 96 | * send orientation 97 | * add external->external transfer support (use a robot slot) 98 | * drop items function in interface 99 | * make water/lava transparent 100 | * use furnaces automatically during crafting 101 | * improve rendering color schemes 102 | * make account registration create a new robot if server is on no-player mode 103 | * set waypoints to account for complicated pathing 104 | * how to split up a selection among multiple robots? 105 | * vertically should work fine most of the time 106 | 107 | -------------------------------------------------------------------------------- /documentation/protocol.md: -------------------------------------------------------------------------------- 1 | Messages are sent in JSON format, using either raw TCP with a \n delimiter or WebSockets. A message object has one key from a list here. The form of the value at that key is described in the schemas for [messages from robots](../public/js/shared/fromRobotSchemas.js) and [messages from web clients](../public/js/shared/fromClientSchemas.js), but here are some easier to read examples. 2 | 3 | ### sent from robot to server 4 | * message 5 | ``` 6 | { 7 | "message": "Hello, World!" 8 | } 9 | ``` 10 | 11 | * command result 12 | ``` 13 | { 14 | "command result": [false, "position already occupied"] 15 | } 16 | ``` 17 | 18 | * map data 19 | ``` 20 | { 21 | "map data": { 22 | x: 0, 23 | z: 0, 24 | y: 0, 25 | w: 3, 26 | d: 3, 27 | data: { 28 | 1: 1, 29 | 2: 1, 30 | 3: 1, 31 | 4: 1, 32 | 5: 1, 33 | 6: 1, 34 | 7: 1, 35 | 8: 1, 36 | 9: 1, 37 | 10: 2, 38 | 11: .6, 39 | 12: 1, 40 | 13: 1, 41 | 14: 0, 42 | 15: 1, 43 | 16: -1, 44 | 17: 1, 45 | 18: 1, 46 | 19: 1, 47 | 20: 1, 48 | 21: 1, 49 | 22: 1, 50 | 23: 0, 51 | 24: 1, 52 | 25: 1, 53 | 26: 1, 54 | 27: 1, 55 | n: 27 56 | } 57 | } 58 | } 59 | ``` 60 | 61 | * block data 62 | ``` 63 | { 64 | "block data": { 65 | name: 'minecraft:dirt', 66 | hardness: .5, 67 | point: { 68 | x: 2, 69 | y: 2, 70 | z: 2, 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | * robot position 77 | ``` 78 | { 79 | "robot position": { 80 | x: 4, 81 | y: 4, 82 | z: 4, 83 | } 84 | } 85 | ``` 86 | 87 | * delete selection 88 | ``` 89 | { 90 | "delete selection": 1 91 | } 92 | ``` 93 | 94 | * dig success 95 | ``` 96 | { 97 | "dig success": { 98 | x: 2, 99 | y: 2, 100 | z: 2, 101 | } 102 | } 103 | ``` 104 | 105 | * inventory data 106 | ``` 107 | { 108 | "inventory data": { 109 | 'size': 64, 110 | 'side': -1, 111 | 'selected': 1 112 | } 113 | } 114 | ``` 115 | 116 | * slot data 117 | ``` 118 | { 119 | "slot data": { 120 | robot: 'rob', 121 | data: { 122 | side: -1, 123 | slotNum: 1, 124 | contents: { 125 | damage: 0, 126 | hasTag: false, 127 | label: 'Dirt', 128 | maxDamage: 0, 129 | maxSize: 64, 130 | name: 'minecraft:dirt', 131 | size: 64 132 | } 133 | } 134 | } 135 | } 136 | ``` 137 | 138 | * power level 139 | ``` 140 | { 141 | "power level": .678 142 | } 143 | ``` 144 | 145 | * available components 146 | ``` 147 | { 148 | "available components": {raw: false} 149 | } 150 | ``` 151 | 152 | 153 | ### sent from web client to server 154 | * scanArea 155 | ``` 156 | { 157 | "command": { 158 | "name": "scanArea", 159 | "parameters": [1] 160 | }, 161 | "robot": "rob" 162 | } 163 | ``` 164 | 165 | * viewInventory 166 | ``` 167 | { 168 | "command": { 169 | "name": "viewInventory", 170 | "parameters": [] 171 | }, 172 | "robot": "rob" 173 | } 174 | ``` 175 | 176 | * equip 177 | ``` 178 | { 179 | "command": { 180 | "name": "equip", 181 | "parameters": [] 182 | }, 183 | "robot": "rob" 184 | } 185 | ``` 186 | 187 | * dig 188 | ``` 189 | { 190 | "command": { 191 | "name": "dig", 192 | "parameters": [1, 2, 3, 4, 5, 6, 1, 1] 193 | }, 194 | "robot": "rob" 195 | } 196 | ``` 197 | 198 | * place 199 | ``` 200 | { 201 | "command": { 202 | "name": "place", 203 | "parameters": [1, 2, 3, 4, 5, 6, 1, 1] 204 | }, 205 | "robot": "rob" 206 | } 207 | ``` 208 | 209 | * move 210 | ``` 211 | { 212 | "command": { 213 | "name": "move", 214 | "parameters": [1, 2, 3, 1] 215 | }, 216 | "robot": "rob" 217 | } 218 | ``` 219 | 220 | * interact 221 | ``` 222 | { 223 | "command": { 224 | "name": "interact", 225 | "parameters": [1, 2, 3, 1] 226 | }, 227 | "robot": "rob" 228 | } 229 | ``` 230 | 231 | * inspect 232 | ``` 233 | { 234 | "command": { 235 | "name": "inspect", 236 | "parameters": [1, 2, 3, 1] 237 | }, 238 | "robot": "rob" 239 | } 240 | ``` 241 | 242 | * select 243 | ``` 244 | { 245 | "command": { 246 | "name": "select", 247 | "parameters": [7] 248 | }, 249 | "robot": "rob" 250 | } 251 | ``` 252 | 253 | * transfer 254 | ``` 255 | { 256 | "command": { 257 | "name": "transfer", 258 | "parameters": [1, -1, 2, 3, 63] 259 | }, 260 | "robot": "rob" 261 | } 262 | ``` 263 | 264 | * craft 265 | ``` 266 | { 267 | "command": { 268 | "name": "craft", 269 | "parameters": ["Wooden Sword"] 270 | }, 271 | "robot": "rob" 272 | } 273 | ``` 274 | 275 | * raw 276 | ``` 277 | { 278 | "command": { 279 | "name": "raw", 280 | "parameters": ["print('Hello, World!');"] 281 | }, 282 | "robot": "rob" 283 | } 284 | ``` 285 | 286 | * sendPosition 287 | ``` 288 | { 289 | "command": { 290 | "name": "sendPosition", 291 | "parameters": [] 292 | }, 293 | "robot": "rob" 294 | } 295 | ``` 296 | 297 | * sendComponents 298 | ``` 299 | { 300 | "command": { 301 | "name": "sendComponents", 302 | "parameters": [] 303 | }, 304 | "robot": "rob" 305 | } 306 | ``` -------------------------------------------------------------------------------- /public/js/server/SocketToAccountMap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An organized way to store sockets and their account details. 3 | */ 4 | class SocketToAccountMap { 5 | 6 | /** 7 | * An organized way to store sockets and their account details. 8 | */ 9 | constructor() { 10 | this.accounts = {}; 11 | this.delimiter = '\n'; 12 | } 13 | 14 | /** 15 | * Get all the web client sockets listening of a particular account name. 16 | * @param {string} accountName 17 | * @returns {SocketIO.Socket[]} 18 | */ 19 | getClients(accountName) { 20 | var result = []; 21 | 22 | var account = this.accounts[accountName]; 23 | if (account && account.clients) { 24 | result = account.clients; 25 | } 26 | 27 | return result; 28 | } 29 | 30 | /** 31 | * Adds a new web client socket to our list for a particular account. 32 | * @param {string} accountName 33 | * @param {SocketIO.Socket} clientSocket 34 | */ 35 | addClient(accountName, clientSocket) { 36 | this.accounts[accountName] = this.accounts[accountName] || {}; 37 | var account = this.accounts[accountName]; 38 | 39 | account.clients = account.clients || []; 40 | account.clients.push(clientSocket); 41 | 42 | if (!clientSocket.cli) { 43 | for (var robotSocket of this.getRobots(accountName)) { 44 | this.sendRobotStateToClients(robotSocket); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Gets rid of a web client socket from our list for a particular account. 51 | * @param {string} accountName 52 | * @param {SocketIO.Socket} clientSocket 53 | * @returns {boolean} 54 | */ 55 | removeClient(accountName, clientSocket) { 56 | clientSocket.removeAllListeners(); 57 | var result = false; 58 | var account = this.accounts[accountName]; 59 | if (account && account.clients) { 60 | account.clients = account.clients.filter(socket => socket !== clientSocket); 61 | result = true; 62 | } 63 | return result; 64 | } 65 | 66 | /** 67 | * Send something to all web clients listening for a particular account. 68 | * @param {string} accountName 69 | * @param {string} eventName 70 | * @param {object} data 71 | * @returns {boolean} 72 | */ 73 | sendToClients(accountName, eventName, data) { 74 | var result = false; 75 | this.getClients(accountName).forEach((clientSocket)=>{ 76 | clientSocket.emit(eventName, data); 77 | result = true; 78 | }); 79 | return result; 80 | } 81 | 82 | /** 83 | * Get the socket for all robots of a certain account. 84 | * @param {string} accountName 85 | * @returns {object[]} 86 | */ 87 | getRobots(accountName) { 88 | var result = []; 89 | 90 | var account = this.accounts[accountName]; 91 | if (account && account.robots) { 92 | result = Object.keys(account.robots).filter(key=>account.robots[key]).map(key => account.robots[key]); 93 | } 94 | 95 | return result; 96 | } 97 | 98 | /** 99 | * Get the socket for a certain robot of a certain account. 100 | * @param {string} accountName 101 | * @param {string} robotName 102 | * @returns {object} 103 | */ 104 | getRobot(accountName, robotName) { 105 | var result = undefined; 106 | 107 | var account = this.accounts[accountName]; 108 | if (account && account.robots) { 109 | result = account.robots[robotName]; 110 | } 111 | 112 | return result; 113 | } 114 | 115 | /** 116 | * Set the socket for a certain robot of a certain account. 117 | * @param {string} accountName 118 | * @param {string} robotName 119 | * @param {object} robotSocket 120 | * @returns {boolean} 121 | */ 122 | setRobot(accountName, robotName, robotSocket) { 123 | this.accounts[accountName] = this.accounts[accountName] || {}; 124 | var account = this.accounts[accountName]; 125 | 126 | account.robots = account.robots || {}; 127 | account.robots[robotName] = robotSocket; 128 | 129 | if (robotSocket) {this.sendRobotStateToClients(robotSocket);} 130 | } 131 | 132 | /** 133 | * Send some data to a specific robot of a specific account. 134 | * @param {string} accountName 135 | * @param {string} robotName 136 | * @param {object} commandInformation 137 | * @returns {boolean} 138 | */ 139 | sendToRobot(accountName, robotName, commandInformation) { 140 | let result = false; 141 | var robotSocket = this.getRobot(accountName, robotName); 142 | if (robotSocket) { 143 | robotSocket.write(JSON.stringify(commandInformation) + this.delimiter); 144 | result = true; 145 | } 146 | return result; 147 | } 148 | 149 | /** 150 | * Used when robots or web clients connect to the server to send the current state of robots. 151 | * @param {object} robotSocket 152 | */ 153 | sendRobotStateToClients(robotSocket) { 154 | this.sendToClients(robotSocket.id.account, "listen start", {robot: robotSocket.id.robot}); 155 | this.sendToRobot(robotSocket.id.account, robotSocket.id.robot, {name: "sendPosition", parameters: []}); 156 | this.sendToRobot(robotSocket.id.account, robotSocket.id.robot, {name: "scanArea", parameters: [1]}); 157 | this.sendToRobot(robotSocket.id.account, robotSocket.id.robot, {name: "sendComponents", parameters: []}); 158 | } 159 | 160 | } 161 | 162 | try { 163 | module.exports = SocketToAccountMap; 164 | } 165 | catch(e) {;} -------------------------------------------------------------------------------- /documentation/tips.md: -------------------------------------------------------------------------------- 1 | ## Usage Tips 2 | 3 | * To see the controls and shortcuts available, press "?" (Shift + /) 4 | 5 | * All buttons and fields in the UI have helpful tooltips you can view by hovering the cursor over them. 6 | 7 | * Clicking on the map will lock the cursor in place, allowing you to control the camera. 8 | 9 | * All commands are issued to the currently selected robot. Change which robot is selected with the dropdown in the top right. 10 | 11 | * If you send a new command to a robot while it's already running one, it will start the second command as soon as it finishes the first. 12 | 13 | * Wondering why the colors of the map are so weird? [See here.](faq.md) 14 | 15 | ### Tools 16 | 17 | * The five buttons in the Tools panel change what happens at your cursor location when you click while controlling the camera. 18 | 19 | * With the move tool selected, the robot will attempt to move to the highlighted point. This can fail if the movement is complex. If that happens, break the movement up into a few smaller ones. 20 | 21 | * With the interact tool selected, the robot will perform a right-click on the highlighted point using whatever tool the robot has equipped. This can be used for things like flipping levers and shearing sheep. 22 | 23 | * If you use the interact tool on a container such as a chest, its contents will be displayed to you, and you can put items in or take them out. Some blocks such as furnaces do not allow all displayed inventory slots to be used, and have different inventories depending on which side you access. 24 | 25 | * With the inspect tool selected, the robot will report in the command history panel exactly what is at the highlighted point. 26 | 27 | * The swing and place tools require you to select an area to operate on. With either of these tools selected, first click on one corner of the area you want to change. Next, click on the opposite corner of the area you want to change. Now you can either click a third time to finalize the area, or change it using the coordinate boxes on the right side of the screen. If you want to stop before finalizing, right click while controlling the camera, or click a button for a separate tool, and your selection will be undone. 28 | 29 | * With the swing tool selected, the robot will perform a left click action with its equipped tool on every block in the specified area. This can be used for things like digging and attacking. 30 | 31 | * With the place tool selected, the robot will attempt to place the block in its selected inventory slot (not the equip slot) at every empty space in the specified area. 32 | 33 | ### Actions 34 | 35 | * The inventory button toggles the visibility of the robot's inventory. Closing and opening the inventory will update it. 36 | Drag and drop an item stack to move it, and click on a slot to select it. 37 | 38 | * The equip button exchanges the items in your selected slot and your equip slot. 39 | 40 | * The scan button reveals the area around the robot, with blocks colored by hardness. The scan size selector affects how large the scanned area will be. For slower computers, a large scan can be rather taxing, so use with care. A small scan will complete almost instantly, but you'll have to wait about 15 to 45 seconds for a large scan. 41 | 42 | * The center button will move the camera to be centered above the selected robot, looking down. 43 | 44 | * The list of items that can be crafted includes all items in Minecraft 1.11 and OpenComputers 1.6. It isn't yet possible to add your own recipes or change the existing ones. 45 | 46 | * The cutaway tool allows easy viewing of layers of ground that are obscured by the surface. 47 | 48 | ### Robot 49 | 50 | * Run `lua /home/lib/commandLoop.lua` to have the robot begin listening for commands from the web client. 51 | 52 | * If you want the robot to begin listening to the Roboserver every time it starts, add the above command to the end of ```/home/.shrc```. 53 | 54 | * Configuration settings for the robot are stored in ```/home/lib/config.txt```. 55 | 56 | * You can easily modify configuration settings using the following command: ```lua /home/lib/config.lua settingName settingValue```. This can be useful for changing the coordinates and orientation of your robot. 57 | 58 | * If you want to be able to send arbitrary Lua code to your robot from the web interface, edit ```/home/lib/config.txt``` and change ```components={}``` to ```components={raw=true}```. This will cause a command input field to appear in the command history panel when you have the configured robot selected. 59 | 60 | * The command input field appends a return to the front of whatever you enter, which is convenient sometimes and inconvenient others. If you have statements reporting syntax errors that shouldn't be, this is likely why. You can work around it if you need to by wrapping your code in a function. 61 | 62 | ### Config 63 | 64 | Here's a list of what each possible setting in config.txt is for. 65 | * robotName: A unique name for your robot, used to tell it apart from other robots. 66 | * accountName: Your Roboserver account name. 67 | * serverIP: The IP address or hostname of your Roboserver. The default is 127.0.0.1. If you're using the standalone application, you don't need to change this. 68 | * tcpPort: the TCP port for your Roboserver. The default is 3001. Don't change this, it's not properly configurable yet. 69 | * posX: The X coordinate your robot will be displayed at in the application. 70 | * posY: The Y coordinate your robot will be displayed at in the application. 71 | * posZ: The Z coordinate your robot will be displayed at in the application. 72 | * orient: This should be 0 if your robot is facing South, 1 if East, 2 if North, 3 if West. It affects how the things your robot sees are displayed in the application. 73 | * components: An empty list by default. Adding ```raw=true``` inside it can allow you to run arbitrary Lua code on your robot from the application. In the future, this will be used to report which upgrades your robot has, allowing the use of more specialized robots. 74 | -------------------------------------------------------------------------------- /public/js/shared/fromRobotSchemas.js: -------------------------------------------------------------------------------- 1 | if (!Ajv) {var Ajv = require("ajv")}; 2 | const ajv = Ajv({allErrors: true, $data: true}); 3 | 4 | const validators = { 5 | 6 | inventoryMeta: ajv.compile({ 7 | "properties": { 8 | "size": { 9 | "type": "integer", 10 | "minimum": 1, 11 | "maximum": 64, 12 | }, 13 | "side": { 14 | "type": "integer", 15 | "minimum": -1, 16 | "maximum": 5, 17 | }, 18 | "selected": { 19 | "type": "integer", 20 | "minimum": 1, 21 | "maximum": {"$data": "1/size"}, 22 | }, 23 | }, 24 | "required": ["size", "side"], 25 | "additionalProperties": false, 26 | }), 27 | 28 | inventorySlot: ajv.compile({ 29 | "properties": { 30 | "side": { 31 | "type": "integer", 32 | "minimum": -1, 33 | "maximum": 5, 34 | }, 35 | "slotNum": { 36 | "type": "integer", 37 | "minimum": 1, 38 | "maximum": 64, 39 | }, 40 | "contents": { 41 | "properties": { 42 | "name": {"type": "string"}, 43 | "label": {"type": "string"}, 44 | "hasTag": {"type": "boolean"}, 45 | "maxSize": { 46 | "type": "integer", 47 | "minimum": 1, 48 | "maximum": 64, 49 | }, 50 | "size": { 51 | "type": "integer", 52 | "minimum": 1, 53 | "maximum": {"$data": "1/maxSize"}, 54 | }, 55 | "maxDamage": { 56 | "type": "integer", 57 | "minimum": 0, 58 | }, 59 | "damage": { 60 | "type": "integer", 61 | "minimum": 0, 62 | "maximum": {"$data": "1/maxDamage"}, 63 | }, 64 | }, 65 | "required": ["name", "label", "hasTag", "size", "maxSize", "damage", "maxDamage"], 66 | }, 67 | }, 68 | "required": ["side", "slotNum"], 69 | "additionalProperties": false, 70 | }), 71 | 72 | position: ajv.compile({ 73 | "properties": { 74 | "x": {"type": "integer",}, 75 | "y": {"type": "integer",}, 76 | "z": {"type": "integer",}, 77 | }, 78 | "required": ["x", "y", "z"], 79 | "additionalProperties": false, 80 | }), 81 | 82 | geolyzerScan: ajv.compile({ 83 | "properties": { 84 | "x": {"type": "integer",}, 85 | "y": {"type": "integer",}, 86 | "z": {"type": "integer",}, 87 | "w": {"type": "integer",}, 88 | "d": {"type": "integer",}, 89 | "data": { 90 | "patternProperties": { 91 | /* 92 | i'm not going to try to make a regex that only 93 | matches numbers up to n, so this schema does 94 | validate some things which are not correct. 95 | */ 96 | "\\d+": {"type": "number"} 97 | }, 98 | "properties": { 99 | "n": {"type": "integer"} 100 | }, 101 | "required": ["n"], 102 | "additionalProperties": false, 103 | }, 104 | }, 105 | "required": ["x", "y", "z", "w", "d", "data"], 106 | "additionalProperties": false, 107 | }), 108 | 109 | components: ajv.compile({ 110 | "patternProperties": { 111 | "^.*$": {"type": "string",}, 112 | }, 113 | "additionalProperties": false, 114 | }), 115 | 116 | config: ajv.compile({ 117 | "properties": { 118 | "robotName": {"type": "string"}, 119 | "accountName": {"type": "string"}, 120 | "serverIP": {"type": "string"}, 121 | "serverPort": {"type": "integer"}, 122 | "tcpPort": {"type": "integer"}, 123 | "posX": {"type": "integer"}, 124 | "posY": {"type": "integer"}, 125 | "posZ": {"type": "integer"}, 126 | "orient": {"type": "integer"}, 127 | "raw": {"type": "boolean",}, 128 | }, 129 | "additionalProperties": false, 130 | }), 131 | 132 | commandResult: ajv.compile({ 133 | "type": "array", 134 | "items": [ 135 | { "type": "string" }, 136 | { "type": ["number", "string", "boolean", "object", "array", "null"] }, 137 | ], 138 | "additionalItems": false, 139 | "minItems": 1, 140 | "maxItems": 2, 141 | }), 142 | 143 | id: ajv.compile({ 144 | "properties": { 145 | "robot": {"type": "string",}, 146 | "account": {"type": "string",}, 147 | }, 148 | "additionalProperties": false, 149 | "required": ["robot", "account"], 150 | }), 151 | 152 | powerLevel: ajv.compile({ 153 | "type": ["number"], 154 | "minimum": 0, 155 | // no max because creative robots have Infinity power 156 | }), 157 | 158 | message: ajv.compile({ 159 | "type": "string", 160 | }), 161 | 162 | digSuccess: ajv.compile({ 163 | "properties": { 164 | "x": {"type": "integer",}, 165 | "y": {"type": "integer",}, 166 | "z": {"type": "integer",}, 167 | }, 168 | "required": ["x", "y", "z"], 169 | "additionalProperties": false, 170 | }), 171 | 172 | deleteSelection: ajv.compile({ 173 | "type": "integer", 174 | }), 175 | 176 | blockData: ajv.compile({ 177 | "properties": { 178 | "name": {"type": "string",}, 179 | "hardness": {"type": "number",}, 180 | "point": { 181 | "properties": { 182 | "x": {"type": "integer",}, 183 | "y": {"type": "integer",}, 184 | "z": {"type": "integer",}, 185 | }, 186 | "required": ["x", "y", "z"], 187 | "additionalProperties": false, 188 | }, 189 | }, 190 | "required": ["name", "hardness", "point"], 191 | }), 192 | 193 | }; 194 | 195 | const keyToValidatorMap = { 196 | 'inventory data': validators.inventoryMeta, 197 | 'slot data': validators.inventorySlot, 198 | 'command result': validators.commandResult, 199 | 'robot position': validators.position, 200 | 'available components': validators.components, 201 | 'map data': validators.geolyzerScan, 202 | 'id': validators.id, 203 | 'message': validators.message, 204 | 'power level': validators.powerLevel, 205 | 'dig success': validators.digSuccess, 206 | 'delete selection': validators.deleteSelection, 207 | 'block data': validators.blockData, 208 | 'config': validators.config, 209 | }; 210 | 211 | try { 212 | module.exports = { 213 | validators: validators, 214 | keyToValidatorMap: keyToValidatorMap, 215 | }; 216 | } 217 | catch (e) {;} -------------------------------------------------------------------------------- /public/css/bootstrap-select.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap-select v1.12.2 (http://silviomoreto.github.io/bootstrap-select) 3 | * 4 | * Copyright 2013-2017 bootstrap-select 5 | * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) 6 | */select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\9}.bootstrap-select>.dropdown-toggle{width:100%;padding-right:25px;z-index:1}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle{border-color:#b94a48}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select.btn-group:not(.input-group-btn),.bootstrap-select.btn-group[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.btn-group.dropdown-menu-right,.bootstrap-select.btn-group[class*=col-].dropdown-menu-right,.row .bootstrap-select.btn-group[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select.btn-group,.form-horizontal .bootstrap-select.btn-group,.form-inline .bootstrap-select.btn-group{margin-bottom:0}.form-group-lg .bootstrap-select.btn-group.form-control,.form-group-sm .bootstrap-select.btn-group.form-control{padding:0}.form-group-lg .bootstrap-select.btn-group.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.btn-group.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.form-inline .bootstrap-select.btn-group .form-control{width:100%}.bootstrap-select.btn-group.disabled,.bootstrap-select.btn-group>.disabled{cursor:not-allowed}.bootstrap-select.btn-group.disabled:focus,.bootstrap-select.btn-group>.disabled:focus{outline:0!important}.bootstrap-select.btn-group.bs-container{position:absolute;height:0!important;padding:0!important}.bootstrap-select.btn-group.bs-container .dropdown-menu{z-index:1060}.bootstrap-select.btn-group .dropdown-toggle .filter-option{display:inline-block;overflow:hidden;width:100%;text-align:left}.bootstrap-select.btn-group .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.bootstrap-select.btn-group[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select.btn-group .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select.btn-group .dropdown-menu li{position:relative}.bootstrap-select.btn-group .dropdown-menu li.active small{color:#fff}.bootstrap-select.btn-group .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select.btn-group .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select.btn-group .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select.btn-group .dropdown-menu li a span.check-mark{display:none}.bootstrap-select.btn-group .dropdown-menu li a span.text{display:inline-block}.bootstrap-select.btn-group .dropdown-menu li small{padding-left:.5em}.bootstrap-select.btn-group .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option{position:static}.bootstrap-select.btn-group.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark{position:absolute;display:inline-block;right:15px;margin-top:5px}.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before{bottom:auto;top:-3px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after{bottom:auto;top:-3px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} -------------------------------------------------------------------------------- /public/js/client/bannerMessages.mjs: -------------------------------------------------------------------------------- 1 | export const bannerMessages = [ 2 | "As seen on TV!", 3 | "Awesome!", 4 | "100% pure!", 5 | "May contain nuts!", 6 | "Better than Prey!", 7 | "More polygons!", 8 | "Sexy!", 9 | "Limited edition!", 10 | "Flashing letters!", 11 | "It's here!", 12 | "Best in class!", 13 | "It's finished!", 14 | "Kind of dragon free!", 15 | "Excitement!", 16 | "More than 500 sold!", 17 | "One of a kind!", 18 | "Heaps of hits on YouTube!", 19 | "Indev!", 20 | "Spiders everywhere!", 21 | "Check it out!", 22 | "Holy cow, man!", 23 | "It's a game!", 24 | "Reticulating splines!", 25 | "Minecraft!", 26 | "Yaaay!", 27 | "Singleplayer!", 28 | "Keyboard compatible!", 29 | "Undocumented!", 30 | "Ingots!", 31 | "Exploding creepers!", 32 | "That's no moon!", 33 | "l33t!", 34 | "Create!", 35 | "Survive!", 36 | "Dungeon!", 37 | "Exclusive!", 38 | "The bee's knees!", 39 | "Classy!", 40 | "Wow!", 41 | "Not on steam!", 42 | "Oh man!", 43 | "Awesome community!", 44 | "Pixels!", 45 | "Now with difficulty!", 46 | "Enhanced!", 47 | "90% bug free!", 48 | "Pretty!", 49 | "12 herbs and spices!", 50 | "Fat free!", 51 | "Absolutely no memes!", 52 | "Free dental!", 53 | "Ask your doctor!", 54 | "Minors welcome!", 55 | "Cloud computing!", 56 | "Legal in Finland!", 57 | "Hard to label!", 58 | "Technically good!", 59 | "Bringing home the bacon!", 60 | "Indie!", 61 | "GOTY!", 62 | "Euclidian!", 63 | "Now in 3D!", 64 | "Inspirational!", 65 | "Herregud!", 66 | "Complex cellular automata!", 67 | "Yes, sir!", 68 | "Played by cowboys!", 69 | "Thousands of colors!", 70 | "Try it!", 71 | "Try the mushroom stew!", 72 | "Sensational!", 73 | "Guaranteed!", 74 | "Macroscopic!", 75 | "Bring it on!", 76 | "Random splash!", 77 | "Call your mother!", 78 | "Monster infighting!", 79 | "Loved by millions!", 80 | "Ultimate edition!", 81 | "Freaky!", 82 | "You've got a brand new key!", 83 | "Water proof!", 84 | "Uninflammable!", 85 | "Whoa, dude!", 86 | "All inclusive!", 87 | "Tell your friends!", 88 | "NP is not in P!", 89 | "Music by C418!", 90 | "Livestreamed!", 91 | "Haunted!", 92 | "Polynomial!", 93 | "Terrestrial!", 94 | "All is full of love!", 95 | "Full of stars!", 96 | "Scientific!", 97 | "Cooler than Spock!", 98 | "Collaborate and listen!", 99 | "Never dig down!", 100 | "Take frequent breaks!", 101 | "Not linear!", 102 | "Han shot first!", 103 | "Nice to meet you!", 104 | "Buckets of lava!", 105 | "Ride the pig!", 106 | "Larger than Earth!", 107 | "sqrt(-1) love you!", 108 | "Phobos anomaly!", 109 | "Punching wood!", 110 | "Falling off cliffs!", 111 | "0% sugar!", 112 | "150% hyperbole!", 113 | "Let's danec!", 114 | "Seecret Friday update!", 115 | "Reference implementation!", 116 | "Kiss the sky!", 117 | "20 GOTO 10!", 118 | "Verlet intregration!", 119 | "Do not distribute!", 120 | "Cogito ergo sum!", 121 | "4815162342 lines of code!", 122 | "A skeleton popped out!", 123 | "The sum of its parts!", 124 | "umop-apisdn!", 125 | "OICU812!", 126 | "Bring me Ray Cokes!", 127 | "Finger-licking!", 128 | "Thematic!", 129 | "Pneumatic!", 130 | "Sublime!", 131 | "Octagonal!", 132 | "Une baguette!", 133 | "Gargamel plays it!", 134 | "Rita is the new top dog!", 135 | "SWM forever!", 136 | "Representing Edsbyn!", 137 | "Supercalifragilisticexpialidocious!", 138 | "Consummate V's!", 139 | "Cow Tools!", 140 | "Double buffered!", 141 | "Fan fiction!", 142 | "Flaxkikare!", 143 | "Jason! Jason! Jason!", 144 | "Hotter than the sun!", 145 | "Internet enabled!", 146 | "Autonomous!", 147 | "Engage!", 148 | "Fantasy!", 149 | "DRR! DRR! DRR!", 150 | "Kick it root down!", 151 | "Regional resources!", 152 | "Woo, facepunch!", 153 | "Woo, somethingawful!", 154 | "Woo, /v/!", 155 | "Woo, tigsource!", 156 | "Woo, minecraftforum!", 157 | "Woo, worldofminecraft!", 158 | "Woo, reddit!", 159 | "Woo, 2pp!", 160 | "Google anlyticsed!", 161 | "Now supports åäö!", 162 | "Give us Gordon!", 163 | "Tip your waiter!", 164 | "Very fun!", 165 | "12345 is a bad password!", 166 | "Vote for net neutrality!", 167 | "Lives in a pineapple under the sea!", 168 | "MAP11 has two names!", 169 | "Omnipotent!", 170 | "Gasp!", 171 | "...!", 172 | "Bees, bees, bees, bees!", 173 | "Jag känner en bot!", 174 | "This text is hard to read if you play the game at the default resolution, but at 1080p it's fine!", 175 | "Haha, LOL!", 176 | "Hampsterdance!", 177 | "Switches and ores!", 178 | "Menger sponge!", 179 | "idspispopd!", 180 | "Eple (original edit)!", 181 | "So fresh, so clean!", 182 | "Slow acting portals!", 183 | "Try the Nether!", 184 | "Don't look directly at the bugs!", 185 | "Oh, ok, Pigmen!", 186 | "Finally with ladders!", 187 | "Scary!", 188 | "Play Minecraft, Watch Topgear, Get Pig!", 189 | "Twittered about!", 190 | "Jump up, jump up, and get down!", 191 | "Joel is neat!", 192 | "A riddle, wrapped in a mystery!", 193 | "Huge tracts of land!", 194 | "Welcome to your Doom!", 195 | "Stay a while, stay forever!", 196 | "Stay a while and listen!", 197 | "Treatment for your rash!", 198 | "Information wants to be free!", 199 | "Lots of truthiness!", 200 | "The creeper is a spy!", 201 | "Turing complete!", 202 | "It's groundbreaking!", 203 | "Let our battle's begin!", 204 | "The sky is the limit!", 205 | "Jeb has amazing hair!", 206 | "Casual gaming!", 207 | "Undefeated!", 208 | "Kinda like Lemmings!", 209 | "Follow the train, CJ!", 210 | "Leveraging synergy!", 211 | "This message will never appear on the splash screen, isn't that weird?", 212 | "DungeonQuest is unfair!", 213 | "110813!", 214 | "90210!", 215 | "Check out the far lands!", 216 | "Tyrion would love it!", 217 | "Also try VVVVVV!", 218 | "Also try Super Meat Boy!", 219 | "Also try Terraria!", 220 | "Also try Mount And Blade!", 221 | "Also try Project Zomboid!", 222 | "Also try World of Goo!", 223 | "Also try Limbo!", 224 | "Also try Pixeljunk Shooter!", 225 | "Also try Braid!", 226 | "That's super!", 227 | "Bread is pain!", 228 | "Read more books!", 229 | "Khaaaaaaaaan!", 230 | "Less addictive than TV Tropes!", 231 | "More addictive than lemonade!", 232 | "Bigger than a bread box!", 233 | "Millions of peaches!", 234 | "Fnord!", 235 | "This is my true form!", 236 | "Totally forgot about Dre!", 237 | "Don't bother with the clones!", 238 | "Pumpkinhead!", 239 | "Hobo humping slobo babe!", 240 | "Made by Jeb!", 241 | "Has an ending!", 242 | "Finally complete!", 243 | "Feature packed!", 244 | "Boots with the fur!", 245 | "Stop, hammertime!", 246 | "Testificates!", 247 | "Conventional!", 248 | "Homeomorphic to a 3-sphere!", 249 | "Doesn't avoid double negatives!", 250 | "Place ALL the blocks!", 251 | "Does barrel rolls!", 252 | "Meeting expectations!", 253 | "PC gaming since 1873!", 254 | "Ghoughpteighbteau tchoghs!", 255 | "Déjà vu!", 256 | "Déjà vu!", 257 | "Got your nose!", 258 | "Haley loves Elan!", 259 | "Afraid of the big, black bat!", 260 | "Doesn't use the U-word!", 261 | "Child's play!", 262 | "See you next Friday or so!", 263 | "150 bpm for 400000 minutes!", 264 | "Technologic!", 265 | "Funk soul brother!", 266 | "Pumpa kungen!", 267 | "日本ハロー!", 268 | "한국 안녕하세요!", 269 | "Helo Cymru!", 270 | "Cześć Polska!", 271 | "你好中国!", 272 | "Привет Россия!", 273 | "Γεια σου ελλάδα!", 274 | "My life for Aiur!", 275 | "Lennart lennart = new Lennart();", 276 | "I see your vocabulary has improved!", 277 | "Who put it there?", 278 | "You can't explain that!", 279 | "if not ok then return end", 280 | "§1C§2o§3l§4o§5r§6m§7a§8t§9i§ac", 281 | "§kFUNKY LOL", 282 | ]; -------------------------------------------------------------------------------- /public/js/server/robotTest.js: -------------------------------------------------------------------------------- 1 | const testData = require('./robotTestData'); 2 | const validators = require('../shared/fromRobotSchemas.js').validators; 3 | const assert = require('assert'); 4 | const TestClient = require('./TestClient'); 5 | const runTests = require('./runTests.js'); 6 | 7 | function setup(testData) { 8 | return new (TestClient)(testData); 9 | } 10 | 11 | let tests = { 12 | 13 | testGeolyzerScan: (testClient)=>{ 14 | let x = -3; 15 | let z = -3; 16 | let y = -2; 17 | let w = 8; 18 | let d = 8; 19 | let scan = testClient.geolyzerScan(x, z, y, w, d, 8); 20 | validators.geolyzerScan(scan); 21 | 22 | /** 23 | * Gets the data index of a particular coordinate in 24 | * a geolyzer scan. 25 | * @param {number} x 26 | * @param {number} y 27 | * @param {number} z 28 | * @return {number} 29 | */ 30 | function getIndex(x, y, z) {return (x + 1) + z*w + y*w*d;} 31 | 32 | /** 33 | * Compares scanned hardness values with map hardness values 34 | * at the given coordinate to make sure they match. 35 | * @param {number} mapX 36 | * @param {number} mapY 37 | * @param {number} mapZ 38 | */ 39 | function testCoord(mapX, mapY, mapZ) { 40 | let scanX = mapX - scan.x; 41 | let scanY = mapY - scan.y; 42 | let scanZ = mapZ - scan.z; 43 | let scanIndex = getIndex(scanX, scanY, scanZ); 44 | let scanHardness = scan.data[scanIndex]; 45 | let clientMapHardness = testClient.map.get(mapX, mapY, mapZ).hardness || 0; 46 | assert(scanHardness == clientMapHardness); 47 | } 48 | 49 | // a coordinate with a block 50 | testCoord(2, 2, 2); 51 | // a coordinate without a block 52 | testCoord(3, 3, 3); 53 | // the test client's position 54 | testCoord(4, 4, 4); 55 | }, 56 | 57 | testSerializeMeta: (testClient)=>{ 58 | 59 | let inventoryMeta = testClient.serializeMeta(); 60 | validators.inventoryMeta(inventoryMeta); 61 | 62 | assert(inventoryMeta.size == testClient.inventory.size); 63 | assert(inventoryMeta.side == -1); 64 | assert(inventoryMeta.selected == testClient.inventory.selected); 65 | 66 | }, 67 | 68 | testEquip: (testClient)=>{ 69 | 70 | assert(!testClient.equipped); 71 | 72 | let testInventory = testClient.inventory; 73 | let selectedIndex = testInventory.selected; 74 | let selectedSlotStack = testInventory.slots[selectedIndex]; 75 | 76 | // make sure there's an item in the selected slot 77 | assert(testInventory.slots[selectedIndex]); 78 | 79 | testClient.equip(); 80 | 81 | assert.deepEqual(testClient.equipped, selectedSlotStack); 82 | assert(!testInventory.slots[selectedIndex]); 83 | 84 | }, 85 | 86 | testGetBoxPoints: (testClient)=>{ 87 | 88 | let boxPoints = testClient.getBoxPoints(1, 1, 1, 1, 1, 2); 89 | for (point of boxPoints) {validators.digSuccess(point);} 90 | 91 | assert(boxPoints.length == 2); 92 | 93 | assert(boxPoints[0].x == 1); 94 | assert(boxPoints[0].y == 1); 95 | assert(boxPoints[0].z == 1); 96 | 97 | assert(boxPoints[1].x == 1); 98 | assert(boxPoints[1].y == 1); 99 | assert(boxPoints[1].z == 2); 100 | 101 | }, 102 | 103 | testDig: (testClient)=>{ 104 | 105 | assert(testClient.map.get(2, 2, 2)); 106 | testClient.dig(2, 2, 2) 107 | assert(!testClient.map.get(2, 2, 2)); 108 | 109 | }, 110 | 111 | testPlace: (testClient)=>{ 112 | 113 | 114 | 115 | }, 116 | 117 | testMove: (testClient)=>{ 118 | 119 | let pos = testClient.position; 120 | 121 | assert(pos.x == 4); 122 | assert(pos.y == 4); 123 | assert(pos.z == 4); 124 | assert(testClient.map.get(4, 4, 4).hardness == 2); 125 | assert(!testClient.map.get(3, 3, 3).hardness); 126 | 127 | testClient.move(3, 3, 3); 128 | let newPos = testClient.position; 129 | validators.position(newPos); 130 | 131 | assert(newPos.x == 3); 132 | assert(newPos.y == 3); 133 | assert(newPos.z == 3); 134 | assert(testClient.map.get(3, 3, 3).hardness == 2); 135 | assert(!testClient.map.get(4, 4, 4).hardness); 136 | 137 | }, 138 | 139 | testInteract: (testClient)=>{ 140 | 141 | 142 | 143 | }, 144 | 145 | testInspect: (testClient)=>{ 146 | 147 | 148 | 149 | }, 150 | 151 | testSelect: (testClient)=>{ 152 | 153 | assert(testClient.inventory.selected != 4); 154 | testClient.select(4); 155 | assert(testClient.inventory.selected == 4); 156 | 157 | }, 158 | 159 | testMoveItems: (testClient)=>{ 160 | 161 | let inv = testClient.inventory; 162 | let slots = inv.slots; 163 | 164 | // move item to an empty slot 165 | assert(slots[7].label == 'Wooden Sword?'); 166 | assert(!slots[6]); 167 | testClient.moveItems(inv, 7, inv, 6, 1); 168 | assert(slots[6].label == 'Wooden Sword?'); 169 | assert(!slots[7]); 170 | 171 | // combine item stacks 172 | assert(slots[1].size == 64); 173 | assert(slots[2].size == 37); 174 | testClient.moveItems(inv, 1, inv, 2, 1); 175 | assert(slots[1].size == 63); 176 | assert(slots[2].size == 38); 177 | 178 | // split item stack 179 | assert(slots[1].size == 63); 180 | assert(!slots[3]); 181 | testClient.moveItems(inv, 1, inv, 3, 1); 182 | assert(slots[1].size == 62); 183 | assert(slots[3].size == 1); 184 | 185 | // swap different item stacks 186 | assert(slots[6].label == 'Wooden Sword?'); 187 | assert(slots[5].label == 'Stone'); 188 | testClient.moveItems(inv, 6, inv, 5, 1); 189 | assert(slots[6].label == 'Stone'); 190 | assert(slots[5].label == 'Wooden Sword?'); 191 | 192 | // fail to combine different item stacks 193 | assert(slots[6].label == 'Stone'); 194 | assert(slots[6].size == 3); 195 | assert(slots[5].label == 'Wooden Sword?'); 196 | let combineDifferent = testClient.moveItems(inv, 6, inv, 5, 1); 197 | assert(!combineDifferent); 198 | 199 | }, 200 | 201 | testGetPosition: (testClient)=>{ 202 | 203 | let pos = testClient.getPosition(); 204 | validators.position(pos); 205 | assert.deepEqual(pos, testClient.position); 206 | 207 | }, 208 | 209 | testGetComponents: (testClient)=>{ 210 | 211 | let components = testClient.getComponents(); 212 | validators.components(components); 213 | assert.deepEqual(components, testClient.components); 214 | 215 | }, 216 | 217 | testGetID: (testClient)=>{ 218 | 219 | let id = testClient.getID(); 220 | validators.id(id); 221 | 222 | }, 223 | 224 | testDecreasePower: (testClient)=>{ 225 | 226 | let startingPower = testClient.power; 227 | let lowerPower = testClient.decreasePower(); 228 | validators.powerLevel(lowerPower); 229 | assert(startingPower > lowerPower); 230 | 231 | }, 232 | 233 | testPlace: (testClient)=>{ 234 | 235 | assert(testClient.map.get(2, 2, 2)); 236 | assert(!testClient.place(2, 2, 2)); 237 | 238 | assert(!testClient.map.get(3, 3, 3)); 239 | let testBlockData = { 240 | name: 'minecraft:dirt', 241 | hardness: .5, 242 | point: { 243 | x: 3, 244 | y: 3, 245 | z: 3, 246 | }, 247 | } 248 | assert.deepEqual(testClient.place(3, 3, 3), testBlockData); 249 | 250 | }, 251 | 252 | testInspect: (testClient)=>{ 253 | 254 | assert(!testClient.map.get(3, 3, 3)); 255 | let airBlockData = { 256 | name: 'minecraft:air', 257 | hardness: 0, 258 | point: { 259 | x: 3, 260 | y: 3, 261 | z: 3, 262 | }, 263 | } 264 | assert.deepEqual(testClient.inspect(3, 3, 3), airBlockData); 265 | 266 | assert(testClient.map.get(2, 2, 2)); 267 | let testBlockData = { 268 | name: 'minecraft:dirt', 269 | hardness: .5, 270 | point: { 271 | x: 2, 272 | y: 2, 273 | z: 2, 274 | }, 275 | } 276 | assert.deepEqual(testClient.inspect(2, 2, 2), testBlockData); 277 | 278 | }, 279 | 280 | } 281 | 282 | runTests(tests, setup, testData); -------------------------------------------------------------------------------- /public/js/server/robotTestData.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | dimension: 'overworld', 4 | 5 | config: { 6 | robotName: 'rob', 7 | accountName: 'admin', 8 | serverIP: '127.0.0.1', 9 | serverPort: 8080, 10 | tcpPort: 3001, 11 | posX: 4, 12 | posY: 4, 13 | posZ: 4, 14 | orient: 0, 15 | raw: true, 16 | }, 17 | 18 | internalInventory: { 19 | meta: { 20 | 'size': 64, 21 | 'side': -1, 22 | 'selected': 1 23 | }, 24 | slots: [ 25 | { 26 | side: -1, 27 | slotNum: 1, 28 | contents: { 29 | 'damage': 0, 30 | 'hasTag': false, 31 | 'label': "Dirt", 32 | 'maxDamage': 0, 33 | 'maxSize': 64, 34 | 'name': "minecraft:dirt", 35 | 'size': 64 36 | } 37 | }, 38 | { 39 | side: -1, 40 | slotNum: 2, 41 | contents: { 42 | 'damage': 0, 43 | 'hasTag': false, 44 | 'label': "Dirt", 45 | 'maxDamage': 0, 46 | 'maxSize': 64, 47 | 'name': "minecraft:dirt", 48 | 'size': 37 49 | } 50 | }, 51 | { 52 | side: -1, 53 | slotNum: 5, 54 | contents: { 55 | 'damage': 0, 56 | 'hasTag': false, 57 | 'label': "Stone", 58 | 'maxDamage': 0, 59 | 'maxSize': 64, 60 | 'name': "minecraft:stone", 61 | 'size': 3 62 | } 63 | }, 64 | { 65 | side: -1, 66 | slotNum: 7, 67 | contents: { 68 | 'damage': 0, 69 | 'hasTag': false, 70 | 'label': "Wooden Sword?", 71 | 'maxDamage': 100, 72 | 'maxSize': 1, 73 | 'name': "minecraft:wooden_sword?", 74 | 'size': 1 75 | } 76 | }, 77 | { 78 | side: -1, 79 | slotNum: 9, 80 | contents: { 81 | 'damage': 0, 82 | 'hasTag': false, 83 | 'label': "Netherrack", 84 | 'maxDamage': 0, 85 | 'maxSize': 64, 86 | 'name': "minecraft:netherrack", 87 | 'size': 23 88 | } 89 | } 90 | ], 91 | }, 92 | 93 | externalInventory: { 94 | meta: { 95 | 'size': 27, 96 | 'side': 3 97 | }, 98 | slots: [ 99 | { 100 | side: 3, 101 | slotNum: 1, 102 | contents: { 103 | 'damage': 0, 104 | 'hasTag': false, 105 | 'label': "Dirt", 106 | 'maxDamage': 0, 107 | 'maxSize': 64, 108 | 'name': "minecraft:dirt", 109 | 'size': 4 110 | } 111 | }, 112 | { 113 | side: 3, 114 | slotNum: 2, 115 | contents: { 116 | 'damage': 0, 117 | 'hasTag': false, 118 | 'label': "Dirt", 119 | 'maxDamage': 0, 120 | 'maxSize': 64, 121 | 'name': "minecraft:dirt", 122 | 'size': 7 123 | } 124 | }, 125 | { 126 | side: 3, 127 | slotNum: 5, 128 | contents: { 129 | 'damage': 0, 130 | 'hasTag': false, 131 | 'label': "Stone", 132 | 'maxDamage': 0, 133 | 'maxSize': 64, 134 | 'name': "minecraft:stone", 135 | 'size': 25 136 | } 137 | } 138 | ], 139 | }, 140 | 141 | map: { 142 | [-2]: { 143 | 0: { 144 | [-2]: { 145 | "hardness": .5, 146 | "name": "minecraft:dirt", 147 | }, 148 | } 149 | }, 150 | [-1]: { 151 | 0: { 152 | [-1]: { 153 | "hardness": .5, 154 | "name": "minecraft:dirt", 155 | }, 156 | } 157 | }, 158 | 0: { 159 | 0: { 160 | 0: { 161 | "hardness": .5, 162 | "name": "minecraft:dirt", 163 | }, 164 | 1: { 165 | "hardness": .5, 166 | "name": "minecraft:dirt", 167 | }, 168 | 2: { 169 | "hardness": .5, 170 | "name": "minecraft:dirt", 171 | }, 172 | }, 173 | 1: { 174 | 0: { 175 | "hardness": .5, 176 | "name": "minecraft:dirt", 177 | }, 178 | 1: { 179 | "hardness": .5, 180 | "name": "minecraft:dirt", 181 | }, 182 | 2: { 183 | "hardness": .5, 184 | "name": "minecraft:dirt", 185 | }, 186 | }, 187 | 2: { 188 | 0: { 189 | "hardness": .5, 190 | "name": "minecraft:dirt", 191 | }, 192 | 1: { 193 | "hardness": .5, 194 | "name": "minecraft:dirt", 195 | }, 196 | 2: { 197 | "hardness": .5, 198 | "name": "minecraft:dirt", 199 | }, 200 | }, 201 | }, 202 | 1: { 203 | 0: { 204 | 0: { 205 | "hardness": .5, 206 | "name": "minecraft:dirt", 207 | }, 208 | 1: { 209 | "hardness": .5, 210 | "name": "minecraft:dirt", 211 | }, 212 | 2: { 213 | "hardness": .5, 214 | "name": "minecraft:dirt", 215 | }, 216 | }, 217 | 1: { 218 | 0: { 219 | "hardness": .5, 220 | "name": "minecraft:dirt", 221 | }, 222 | 1: { 223 | "hardness": 2.5, 224 | "name": "minecraft:chest", 225 | }, 226 | 2: { 227 | "hardness": .5, 228 | "name": "minecraft:dirt", 229 | }, 230 | }, 231 | 2: { 232 | 0: { 233 | "hardness": .5, 234 | "name": "minecraft:dirt", 235 | }, 236 | 2: { 237 | "hardness": .5, 238 | "name": "minecraft:dirt", 239 | }, 240 | }, 241 | }, 242 | 2: { 243 | 0: { 244 | 0: { 245 | "hardness": .5, 246 | "name": "minecraft:dirt", 247 | }, 248 | 1: { 249 | "hardness": .5, 250 | "name": "minecraft:dirt", 251 | }, 252 | 2: { 253 | "hardness": .5, 254 | "name": "minecraft:dirt", 255 | }, 256 | }, 257 | 1: { 258 | 0: { 259 | "hardness": .5, 260 | "name": "minecraft:dirt", 261 | }, 262 | 1: { 263 | "hardness": .5, 264 | "name": "minecraft:dirt", 265 | }, 266 | 2: { 267 | "hardness": .5, 268 | "name": "minecraft:dirt", 269 | }, 270 | }, 271 | 2: { 272 | 0: { 273 | "hardness": .5, 274 | "name": "minecraft:dirt", 275 | }, 276 | 1: { 277 | "hardness": .5, 278 | "name": "minecraft:dirt", 279 | }, 280 | 2: { 281 | "hardness": .5, 282 | "name": "minecraft:dirt", 283 | }, 284 | }, 285 | }, 286 | 3: { 287 | 0: { 288 | 3: { 289 | "hardness": .5, 290 | "name": "minecraft:dirt", 291 | }, 292 | } 293 | }, 294 | 4: { 295 | 0: { 296 | 4: { 297 | "hardness": .5, 298 | "name": "minecraft:dirt", 299 | }, 300 | } 301 | }, 302 | 5: { 303 | 0: { 304 | 5: { 305 | "hardness": .5, 306 | "name": "minecraft:dirt", 307 | }, 308 | } 309 | }, 310 | }, 311 | 312 | components: { 313 | '0c9a47e1-d211-4d73-ad9d-f3f88a6d0ee9': 'keyboard', 314 | '25945f78-9f94-4bd5-9211-cb37b352ebf7': 'filesystem', 315 | '2a2da751-3baa-4feb-9fa1-df76016ff390': 'inventory_controller', 316 | '5d48c968-e6f5-474e-865f-524e803678a7': 'filesystem', 317 | '5f6a65bd-98c1-4cf5-bbe5-3e0b8b23662d': 'internet', 318 | '5fffe6b3-e619-4f4f-bc9f-3ddb028afe02': 'filesystem', 319 | '73aedc53-517d-4844-9cb2-645c8d7667fc': 'screen', 320 | '8be5872b-9a01-4403-b853-d88fe6f17fc3': 'eeprom', 321 | '9d40d271-718e-45cc-9522-dc562320a78b': 'crafting', 322 | 'c8181ac9-9cc2-408e-b214-3acbce9fe1c6': 'chunkloader', 323 | 'c8fe2fde-acb0-4188-86ef-4340de011e25': 'robot', 324 | 'ce229fd1-d63d-4778-9579-33f05fd6af9a': 'gpu', 325 | 'fdc85abb-316d-4aca-a1be-b7f947016ba1': 'computer', 326 | 'ff1699a6-ae72-4f74-bf1c-88ac6901d56e': 'geolyzer' 327 | }, 328 | 329 | }; -------------------------------------------------------------------------------- /public/js/client/WebClient.mjs: -------------------------------------------------------------------------------- 1 | import {WorldAndScenePoint} from '/js/client/WorldAndScenePoint.mjs'; 2 | import {Robot} from '/js/client/Robot.mjs'; 3 | import {InventoryRender} from '/js/client/InventoryRender.mjs'; 4 | 5 | export class WebClient { 6 | 7 | /** 8 | * Used to make a WebClient ready for communicating with robots. 9 | * @param {Game} game 10 | */ 11 | constructor(game) { 12 | 13 | this.game = game; 14 | 15 | this.socket = io(); 16 | 17 | this.allRobotInfo = {}; 18 | 19 | this.commandMap = { 20 | 21 | 'message': console.dir, 22 | 23 | /** 24 | * Used to display command results received from robots. 25 | * @param {object} result 26 | */ 27 | 'command result': (result)=>{ 28 | console.dir('command result'); 29 | console.dir(result); 30 | this.game.GUI.addMessage(result.data, false); 31 | }, 32 | 33 | /** 34 | * Used to render map data received from robots. 35 | * @param {object} mapData 36 | */ 37 | 'map data': (mapData)=>{ 38 | console.dir('map data'); 39 | console.dir(mapData); 40 | this.game.mapRender.addShapeVoxels(mapData.data); 41 | }, 42 | 43 | /** 44 | * Used to render block data received from robots. 45 | * @param {object} blockData 46 | */ 47 | 'block data': (blockData)=>{ 48 | console.dir('block data'); 49 | console.dir(blockData); 50 | let pos = new WorldAndScenePoint(blockData.data.point, true); 51 | if (!(blockData.data.name == 'minecraft:air' || !blockData.data.name)) { 52 | this.game.mapRender.addVoxel(pos, this.game.mapRender.colorFromHardness(blockData.data.hardness)); 53 | } 54 | else {this.game.mapRender.removeVoxel(pos);} 55 | this.game.GUI.addMessage([`name: ${blockData.data.name}`, `hardness: ${blockData.data.hardness}`]); 56 | }, 57 | 58 | /** 59 | * Used to render the robot's position. 60 | * @param {object} pos 61 | */ 62 | 'robot position': (pos)=>{ 63 | console.dir('robot position'); 64 | console.dir(pos); 65 | this.game.mapRender.moveRobotVoxel(new WorldAndScenePoint(pos.data, true), pos.robot); 66 | this.game.webClient.allRobotInfo[pos.robot].removeAllExternalInventories(); 67 | if (pos.robot == this.game.GUI.robotSelect.value) { 68 | let robotData = this.game.webClient.allRobotInfo[pos.robot]; 69 | if (robotData) { 70 | this.game.mapRender.selectedRobotMesh.position.copy(robotData.getPosition().scene()); 71 | this.game.mapRender.requestRender(); 72 | } 73 | } 74 | }, 75 | 76 | /** 77 | * Used to get rid of selection areas when their task is done. 78 | * @param {object} index 79 | */ 80 | 'delete selection': (index)=>{ 81 | console.dir('delete selection'); 82 | console.dir(index); 83 | this.game.GUI.deleteSelection(index.data); 84 | }, 85 | 86 | /** 87 | * Used to remove voxels after successfully digging the 88 | * corresponding block in the world. 89 | * @param {object} pos 90 | */ 91 | 'dig success': (pos)=>{ 92 | console.dir('dig success'); 93 | console.dir(pos); 94 | this.game.mapRender.removeVoxel(new WorldAndScenePoint(pos.data, true)); 95 | }, 96 | 97 | /** 98 | * Used to render inventory data received from robots. 99 | * @param {object} inventoryData 100 | */ 101 | 'inventory data': (inventoryData)=>{ 102 | 103 | console.dir('inventory data'); 104 | console.dir(inventoryData); 105 | 106 | let inventoryContainer = this.game.GUI.inventoryContainer; 107 | let inventorySide = inventoryData.data.side; 108 | if (!this.allRobotInfo[inventoryData.robot].getInventory(inventorySide)) { 109 | let inv = new InventoryRender(inventoryData.data, this.game.GUI); 110 | console.dir(inv) 111 | this.allRobotInfo[inventoryData.robot].addInventory(inv); 112 | if (this.game.GUI.robotSelect.value == inventoryData.robot) { 113 | inv.addToDisplay(inventoryContainer); 114 | } 115 | } 116 | 117 | // reveal inventories when any change occurs 118 | if (!this.allRobotInfo[inventoryData.robot].getShowInventories()) { 119 | this.allRobotInfo[inventoryData.robot].toggleShowInventories(); 120 | } 121 | inventoryContainer.classList.remove('hidden'); 122 | // get the robot's inventory if we didn't have it yet 123 | if (!this.allRobotInfo[inventoryData.robot].getInventory(-1)) { 124 | this.game.GUI.sendCommand('viewInventory'); 125 | } 126 | 127 | }, 128 | 129 | /** 130 | * Used to render inventory slot data received from robots. 131 | * @param {object} slot 132 | */ 133 | 'slot data': (slot)=>{ 134 | console.dir('slot data'); 135 | console.dir(slot); 136 | this.allRobotInfo[slot.robot] 137 | .getInventory(slot.data.side) 138 | .setSlot(slot.data.slotNum, slot.data.contents); 139 | }, 140 | 141 | /** 142 | * Used to add listening robots to the select field. 143 | * @param {object} data 144 | */ 145 | 'listen start': (data)=>{ 146 | console.dir('listen start'); 147 | console.dir(data); 148 | let robotSelect = this.game.GUI.robotSelect; 149 | if (!robotSelect.querySelector('[value=' + data.robot + ']')) { 150 | 151 | let option = document.createElement('option'); 152 | option.text = data.robot; 153 | option.value = data.robot; 154 | 155 | robotSelect.add(option); 156 | if (robotSelect.options.length <= 2) { 157 | option.selected = true; 158 | this.game.GUI.switchToRobot(data.robot); 159 | } 160 | 161 | } 162 | if (!this.allRobotInfo[data.robot]) {this.allRobotInfo[data.robot] = new Robot();} 163 | }, 164 | 165 | /** 166 | * Used to remove robots that stop listening from the select field. 167 | * @param {object} data 168 | */ 169 | 'listen end': (data)=>{ 170 | console.dir('listen end'); 171 | console.dir(data); 172 | let robotSelect = this.game.GUI.robotSelect; 173 | let option = robotSelect.querySelector('[value=' + data.robot + ']'); 174 | this.allRobotInfo[data.robot] = undefined; 175 | // if the disconnecting robot is the currently selected robot 176 | if (robotSelect.value == data.robot) { 177 | let noRobotName = '' 178 | robotSelect.value = noRobotName; 179 | this.game.GUI.switchToRobot(noRobotName); 180 | } 181 | robotSelect.removeChild(option); 182 | }, 183 | 184 | /** 185 | * Used to display power levels received from robots. 186 | * @param {object} power 187 | */ 188 | 'power level': (power)=>{ 189 | console.dir('power level'); 190 | console.dir(power); 191 | this.allRobotInfo[power.robot].setPower(power.data); 192 | let currentRobot = this.game.GUI.robotSelect.value; 193 | if (power.robot == currentRobot) { 194 | this.game.GUI.setPower(power.data); 195 | } 196 | }, 197 | 198 | /** 199 | * Used to change the GUI based on what components robots have available. 200 | * @param {object} components 201 | */ 202 | 'available components': (components)=>{ 203 | console.dir('available components'); 204 | console.dir(components); 205 | this.allRobotInfo[components.robot].setComponents(components.data); 206 | if (components.robot == this.game.GUI.robotSelect.value) { 207 | this.game.GUI.hideComponentGUI(); 208 | for (let componentName in this.allRobotInfo[components.robot].getComponents()) { 209 | let componentElementNames = this.game.GUI.componentElementMap[componentName]; 210 | componentElementNames.map((componentElementName)=>{ 211 | this.game.GUI[componentElementName].classList.remove('hidden'); 212 | }); 213 | } 214 | } 215 | }, 216 | 217 | }; 218 | 219 | for (let messageName in this.commandMap) { 220 | this.socket.on(messageName, this.commandMap[messageName]); 221 | } 222 | 223 | } 224 | 225 | } -------------------------------------------------------------------------------- /public/js/server/customizeServer.js: -------------------------------------------------------------------------------- 1 | var accounts = new (require('./SocketToAccountMap'))(); 2 | var keyToValidatorMap = require('../shared/fromRobotSchemas').keyToValidatorMap; 3 | var fromClientSchemas = require('../shared/fromClientSchemas'); 4 | 5 | const delimiter = '\n'; 6 | 7 | /** 8 | * Starts the TCP server and adds event listeners to the HTTP server. 9 | * @param {object} server 10 | */ 11 | function main(server, app) { 12 | 13 | // start http/socket.io server code 14 | 15 | var io = require('socket.io')(server); 16 | var config = require('../config/config'); 17 | var request = require('request'); 18 | 19 | var passportSocketIo = require('passport.socketio'); 20 | io.use(passportSocketIo.authorize({ 21 | store: app.get('sessionStore'), 22 | success: onAuthorizeSuccess, 23 | fail: onAuthorizeFail, 24 | secret: config.expressSessionSecret 25 | })); 26 | 27 | function onAuthorizeSuccess(data, accept){ 28 | console.log('successful connection to socket.io'); 29 | console.dir(data.user) 30 | accept(); 31 | } 32 | 33 | function onAuthorizeFail(data, message, error, accept){ 34 | console.log('failed connection to socket.io:', message); 35 | if (error) {throw new Error(message);} 36 | else {accept(new Error(message));} 37 | } 38 | 39 | io.on('connection', function (socket) { 40 | 41 | // cli clients don't need robot state on every connection 42 | socket.cli = socket.request._query.cli; 43 | accounts.addClient(socket.request.user.username, socket); 44 | console.log(socket.request.user.username + " account connected"); 45 | 46 | socket.on('message', (data)=>{ 47 | console.dir(data); 48 | socket.send('pong'); 49 | }); 50 | socket.on('disconnect', function () { 51 | accounts.removeClient(socket.request.user.username, socket); 52 | console.log(socket.request.user.username + " account disconnected"); 53 | request.get(`http://${socket.request.headers.host}/logout`); 54 | console.log(socket.request.user.username + " account logged out"); 55 | }); 56 | 57 | // relay commands to the tcp server 58 | socket.on('command', (data)=>{ 59 | console.dir(data); 60 | if (fromClientSchemas[data.command.name](data)) { 61 | accounts.sendToRobot(socket.request.user.username, data.robot, data.command); 62 | } 63 | else { 64 | let errorString = `command validation failed: ${JSON.stringify(data.command)}`; 65 | console.error('client ' + errorString); 66 | socket.emit('message', errorString); 67 | } 68 | }); 69 | 70 | }); 71 | 72 | // end http/socket.io server code 73 | 74 | // start tcp server code 75 | 76 | // listen for tcp connections from robots 77 | var net = require('net'); 78 | 79 | var tcpServer = net.createServer((tcpSocket)=>{ 80 | 81 | console.log("unidentified robot connected"); 82 | 83 | // test write 84 | tcpSocket.write( 85 | JSON.stringify({name: 'message', parameters: ["hello, it's the tcp server!"]}) + delimiter 86 | ); 87 | 88 | // relay command results from robot to web server 89 | tcpSocket.on('data', (data)=>{ 90 | if (!tcpSocket.remainder) {tcpSocket.remainder = '';} 91 | const parsedTCP = parseTCPData(data.toString(), tcpSocket.remainder); 92 | tcpSocket.remainder = parsedTCP.remainder; 93 | const dataJSONList = parsedTCP.messages.map(JSON.parse); 94 | 95 | // separate tcp data into various messages 96 | for (var dataJSON of dataJSONList) { 97 | for (var key in dataJSON) { 98 | 99 | if (keyToValidatorMap[key](dataJSON[key])) { 100 | 101 | if (key == 'id') { 102 | tcpSocket.id = dataJSON[key]; 103 | if (accounts.getRobot(tcpSocket.id.account, tcpSocket.id.robot)) { 104 | var errorString = 'a robot called ' + tcpSocket.id.robot + 105 | ' is already connected to account ' + tcpSocket.id.account; 106 | disconnectRobot(tcpSocket, errorString); 107 | } 108 | else { 109 | accounts.setRobot(tcpSocket.id.account, tcpSocket.id.robot, tcpSocket); 110 | console.log("robot " + tcpSocket.id.robot + " identified for account " + tcpSocket.id.account); 111 | } 112 | } 113 | else if (tcpSocket.id && tcpSocket.id.account && tcpSocket.id.robot) { 114 | console.log('key', key) 115 | console.log('data', dataJSON[key]) 116 | accounts.sendToClients(tcpSocket.id.account, key, {data: dataJSON[key], robot: tcpSocket.id.robot}); 117 | } 118 | else { 119 | var errorString = 'unidentified robots cannot send messages'; 120 | disconnectRobot(tcpSocket, errorString); 121 | } 122 | 123 | } 124 | 125 | else { 126 | 127 | var errorString = `command ${key} failed to validate with value ${JSON.stringify(dataJSON[key])}`; 128 | console.error('robot ' + errorString); 129 | tcpSocket.write(JSON.stringify({name: 'message', parameters: [errorString]}) + delimiter); 130 | 131 | } 132 | 133 | } 134 | } 135 | 136 | }); 137 | 138 | /** 139 | * Used to piece together any tcp messages which got broken up, 140 | * and find any pieces we don't have the end of yet. 141 | * @param {string} tcpString 142 | * @param {string} tcpRemainder 143 | */ 144 | function parseTCPData(tcpString, tcpRemainder) { 145 | let completeMessages = []; 146 | // this might get weird if the delimiter isn't 147 | const tcpMessageRegExp = new RegExp(`(.*${delimiter}?|${delimiter})`, 'g'); 148 | const tcpMessages = tcpString.match(tcpMessageRegExp) || []; 149 | for (let tcpMessage of tcpMessages) { 150 | 151 | const assembledTCPMessage = tcpRemainder ? tcpRemainder + tcpMessage : tcpMessage; 152 | tcpRemainder = ''; 153 | 154 | const containsDelimiter = assembledTCPMessage.indexOf(delimiter) != -1; 155 | if (!containsDelimiter) { 156 | tcpRemainder = assembledTCPMessage; 157 | } 158 | else { 159 | completeMessages.push(assembledTCPMessage); 160 | } 161 | 162 | } 163 | let result = { 164 | messages: completeMessages, 165 | remainder: tcpRemainder, 166 | }; 167 | return result; 168 | } 169 | 170 | /** 171 | * Disconnects a robot without notifying the web client. 172 | * Used for unidentified robots and duplicate identifiers. 173 | * @param {object} robotSocket 174 | * @param {string} errorString 175 | */ 176 | function disconnectRobot(robotSocket, errorString) { 177 | console.error(errorString); 178 | robotSocket.write(JSON.stringify({name: 'message', parameters: [errorString]}) + delimiter); 179 | robotSocket.endedByServer = true; 180 | robotSocket.end(); 181 | } 182 | 183 | /** 184 | * Tells the web client a robot has disconnected. 185 | * @param {object} robotSocket 186 | */ 187 | function notifyOfDisconnect(robotSocket) { 188 | if (robotSocket.id) { 189 | accounts.setRobot(robotSocket.id.account, robotSocket.id.robot); 190 | accounts.sendToClients(robotSocket.id.account, "listen end", {robot: robotSocket.id.robot}) 191 | console.log("robot " + robotSocket.id.robot + " for account " + robotSocket.id.account + " disconnected"); 192 | } 193 | } 194 | 195 | // Remove the client from the list when it leaves 196 | 197 | // Emitted when an error occurs. The 'close' event will be called directly following this event. 198 | tcpSocket.on('error', (error)=>{ 199 | console.error(error); 200 | }); 201 | 202 | // Emitted when the other end of the socket sends a FIN packet, thus ending the readable side of the socket. 203 | tcpSocket.on('end', ()=>{ 204 | console.log('end'); 205 | if (!tcpSocket.endedByServer) { 206 | // notifyOfDisconnect(tcpSocket); 207 | } 208 | }); 209 | 210 | // Emitted once the socket is fully closed 211 | tcpSocket.on('close', ()=>{ 212 | console.log('closed'); 213 | if (!tcpSocket.endedByServer) { 214 | notifyOfDisconnect(tcpSocket); 215 | } 216 | }); 217 | 218 | }).listen(3001); 219 | 220 | // end tcp server code 221 | 222 | console.log(`Server running! Open localhost:${config.webServerPort} in your browser to see it.\n`) 223 | 224 | } 225 | 226 | module.exports = main; 227 | -------------------------------------------------------------------------------- /testplan.md: -------------------------------------------------------------------------------- 1 | install 2 | * delete all the code, make sure the server install works 3 | * make sure all the unit tests pass 4 | * make new binaries, make sure they work 5 | * create a fresh world to test robots on 6 | 7 | run all tests on a robot built to the minimum requirements: 8 | * gold case 9 | * t2 cpu 10 | * t1 memory x2 11 | * EEPROM (Lua BIOS) 12 | * t1 hard disk drive with OpenOS installed 13 | * internet card 14 | * geolyzer 15 | * inventory controller 16 | * crafting upgrade 17 | * inventory upgrade 18 | 19 | lua installation on robot 20 | * paste install.txt into a robot's terminal 21 | * make sure to test it with and without the server running locally 22 | * easy config should ask you for information 23 | * make sure it successfully connects to the specified server 24 | 25 | config 26 | * change position, orientation, booleans to string equivalents 27 | * change ip address to hostname 28 | * ensure things still work 29 | 30 | (make sure all the ui tests work in chrome, firefox, edge, and electron) 31 | 32 | login 33 | * confirm that account registration fails when it doesn't have a username and a password 34 | * confirm that account registration succeeds with a username and password 35 | * confirm that login fails for nonexistent accounts 36 | * confirm that login fails for incorrect password on existing accounts 37 | * confirm that login succeeds for correct password on existing account 38 | 39 | robot connections on page load 40 | * test page load with no listening robots 41 | * command input box will be hidden, power will be empty, robot select will be empty 42 | * test page load with a listening robot 43 | * confirm that robot sends its location and is rendered 44 | * confirm that robot sends a scan which is rendered 45 | * confirm that the robot select shows the robot's name 46 | * confirm that when the robot's components config contains raw set to true, the command input becomes visible 47 | * confirm that otherwise, the command input remains invisible 48 | * test page load with two listening robots 49 | * make sure everything goes just like the check with one robot 50 | 51 | test first robot connecting after web client is loaded 52 | * confirm that robot sends its location and is rendered 53 | * confirm that robot sends a scan which is rendered 54 | * confirm that the robot select shows the robot's name 55 | * confirm that when the robot has raw set to true, the command input becomes visible 56 | * confirm that when raw is false, the command input remains invisible 57 | 58 | top left panel 59 | * confirm logged in account name is displayed 60 | * after a robot is connected, confirm that its power level is displayed correctly 61 | * confirm that the displayed cursor position updates as it moves 62 | 63 | hotkeys 64 | * confirm that the cursor is lowered into the block you're looking at while alt is held 65 | 66 | robot select 67 | * confirm that when no other robots are listening, if a robot starts to listen it is selected 68 | * confirm that if two robots are listening, both appear in the select 69 | * confirm that these things change when a different robot is selected: 70 | * power level 71 | * selected robot mesh position 72 | * robot select name 73 | * whether the command input is visible 74 | * confirm that switching to a different robot changes the inventories in the UI to those of the new robot 75 | * confirm that switching back to the original robot changes the UI's inventories back again 76 | 77 | tool buttons 78 | * confirm that the move button is pressed upon page load 79 | * confirm that when the swing or place buttons are pressed, the coordinate forms expand 80 | * confirm that when the move, interact, or inspect buttons are pressed, the coordinate forms collapse 81 | 82 | move 83 | * confirm that the robot can move up, down, forward, and can rotate 84 | * confirm that attempting to move to an occupied space fails 85 | * confirm that the robot selected mesh follows the robot 86 | * confirmed that opened external inventories are removed upon move 87 | 88 | interact 89 | * have a robot equip a block and interact with an empty space adjacent to another block 90 | * confirm that the robot now has 1 less of the equipped block 91 | * confirm that interacting with an inventory causes it and the robot's inventory to display 92 | 93 | inspect 94 | * inspect an area that appears empty but actually has a block 95 | * confirm that the block appears 96 | * inspect an area that appears to have a block but does not 97 | * confirm that the block disappears 98 | * inspect an area of uniform blocks 99 | * confirm that they're all rendered as the same hardness afterwards 100 | 101 | swing 102 | * make sure you've got an appropriate tool equipped 103 | * confirm that the robot has more blocks in its inventory after digging an area (min. volume 2) 104 | * confirm that the robot still attempts to dig every point in a selection when some are empty 105 | 106 | place 107 | * select a block 108 | * select an area including empty and occupied spaces 109 | * confirm that blocks are now shown as placed in the empty spaces 110 | * confirm that the inventory updates automatically to show fewer of the selected block present 111 | 112 | coordinate forms 113 | * confirm that clicking once with either the swing or place tools selected fills in the first form 114 | * confirm that with the first form already filled in, clicking with these tools selected fills in the second form 115 | * confirm that after a click with both forms already filled, they are both cleared. 116 | * confirm that the coordinate forms are cleared when the selected tool changes 117 | * what's the current behaviour for partially filled forms? 118 | 119 | craft 120 | * confirm that crafting fails when you don't have the required raw materials 121 | * confirm that recipes with multiple materials and one product work (a bowl from spruce wood logs) 122 | * confirm that recipes with multiple materials and multiple products work (spruce boat from logs) 123 | * confirm that recipes which require a material to be crafted multiple times work (chest from logs) 124 | * confirm that recipes where one material is used to craft another work (wooden pickaxe from logs) 125 | * confirm that the inventory updates during the crafting process 126 | * confirm that crafting succeeds when you do have the required raw materials 127 | 128 | inventory button 129 | * while a robot is selected 130 | * confirm that no inventory is displayed before clicking the inventory button 131 | * confirm that pressing it reveals the robot's inventory 132 | * confirm that pressing it again hides the inventory 133 | * confirm that pressing it a third time reveals an updated inventory 134 | 135 | scan 136 | * confirm that a scan is performed when pressed 137 | * confirm that the robot remains pink and is not overwritten to be gray 138 | 139 | equip 140 | * confirm that the inventory is opened when pressed 141 | * confirm that any item in the selected slot is equipped, and any equipped item is moved to the selected slot. 142 | 143 | center 144 | * confirm that the camera is positioned above the selected robot, looking down 145 | 146 | cutaway 147 | * confirm that moving the cutaway value up or down modifies the display accordingly 148 | * confirm that changing the cutaway axis modifies the display accordingly 149 | * confirm that changing the cutaway operator modifies the display accordingly 150 | 151 | scan size 152 | * confirm that moving and button scans are not performed if set to none 153 | * confirm that moving and button scans of the appropriate size are performed when set to small 154 | * confirm that moving and button scans of the appropriate size are performed when set to large 155 | 156 | command history display 157 | * confirm that the command history panel is not displayed when empty 158 | * confirm that robot responses show up after sending a command 159 | * confirm that clicking a sent command sends it again with the same parameters 160 | 161 | command input 162 | * confirm that attempts to serialize functions do not trigger a tcp reconnect 163 | * confirm that code execution results are reported when the code contains an error 164 | * confirm that code execution results are reported when the code does not contain an error 165 | 166 | inventory logic 167 | * move empty slot onto stack (what's the expected behavior?) 168 | * move stack onto empty slot 169 | * move stack onto different stack (should swap) 170 | * move stack onto similar stack (combined stack size <= max stack size) 171 | * move stack onto similar stack (source partial, target full) 172 | * move stack onto similar stack (source full, target partial) 173 | 174 | * split stack into empty slot 175 | * split stack into different stack 176 | * split stack into similar stack (amount <= space in target) 177 | * split stack into similar stack (amount > space in target) 178 | 179 | * move internal stack to external empty slot 180 | * move external stack to internal empty slot 181 | * fail to move external stack to external empty slot -------------------------------------------------------------------------------- /public/lua/oc/craft.lua: -------------------------------------------------------------------------------- 1 | local component = require("component"); 2 | local robot = component.robot; 3 | local craft = component.crafting.craft; 4 | local inv = component.inventory_controller; 5 | local inet = component.internet; 6 | local string = require("string"); 7 | local JSON = require("json"); 8 | local table = require("table"); 9 | local config = require('config'); 10 | local conf = config.get(config.path); 11 | local int = require('interact'); 12 | 13 | local M = {}; 14 | 15 | local craftingSlots = {1, 2, 3, nil, 5, 6, 7, nil, 9, 10, 11}; 16 | 17 | local invSize = robot.inventorySize(); 18 | 19 | function M.getRecipes(itemName) 20 | itemName = string.gsub(itemName, " ", "%%20"); 21 | itemName = string.gsub(itemName, "/", "%%2F"); 22 | local req = inet.request("http://" .. conf.serverIP .. ':' .. conf.serverPort .. "/recipe/" .. itemName); 23 | local recipeJSON = ""; 24 | local reqLine = req.read(); 25 | while reqLine do 26 | recipeJSON = recipeJSON .. reqLine; 27 | reqLine = req.read(); 28 | end 29 | req.close(); 30 | return JSON:decode(recipeJSON); 31 | end 32 | 33 | function M.getPattern(patternIndex, recipe) 34 | local pattern = {false, false, false, false, false, false, false, false, false}; 35 | for slotNum, slotChoices in pairs(recipe["in"]) do 36 | if #slotChoices >= patternIndex then 37 | pattern[M.convertSlotToGrid(tonumber(slotNum))] = slotChoices[patternIndex][1].product; 38 | else 39 | pattern[M.convertSlotToGrid(tonumber(slotNum))] = slotChoices[1][1].product; 40 | end 41 | end 42 | return pattern; 43 | end 44 | 45 | function M.getRecipeChoiceCount(recipe) 46 | local recipeChoiceCount = 0; 47 | for slotNum, slotChoices in pairs(recipe["in"]) do 48 | if #slotChoices > recipeChoiceCount then 49 | recipeChoiceCount = #slotChoices; 50 | end 51 | end 52 | return recipeChoiceCount; 53 | end 54 | 55 | -- determine if we have enough of an item for a recipe 56 | function M.countInPattern(label, pattern) 57 | local counter = 0; 58 | for i, name in pairs(pattern) do 59 | if label == name then 60 | counter = counter + 1; 61 | end 62 | end 63 | return counter; 64 | end 65 | 66 | function M.countInInventory(label) 67 | local total = 0; 68 | for i = 1, invSize do 69 | if robot.count(i) > 0 then 70 | local slotInfo = inv.getStackInInternalSlot(i); 71 | if slotInfo.label == label then 72 | total = total + slotInfo.size; 73 | end 74 | end 75 | end 76 | return total; 77 | end 78 | 79 | function M.firstAvailableSlot(stackInfo) 80 | local availableSlot = 0; 81 | for i = 1, invSize do 82 | if availableSlot == 0 then 83 | if not craftingSlots[i] then 84 | if not (robot.count(i) > 0) then 85 | availableSlot = i; 86 | else 87 | if stackInfo then 88 | local slotInfo = inv.getStackInInternalSlot(i); 89 | local sameName = stackInfo.label == slotInfo.label; 90 | local stackFits = stackInfo.size + slotInfo.size <= slotInfo.maxSize; 91 | if sameName and stackFits then 92 | availableSlot = i; 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | return availableSlot; 100 | end 101 | 102 | function M.clearCraftingGrid() 103 | local side = -1; 104 | int.sendInventoryMetadata(side); 105 | for i, slot in pairs(craftingSlots) do 106 | if slot then 107 | if robot.count(slot) > 0 then 108 | local slotInfo = inv.getStackInInternalSlot(slot); 109 | robot.select(slot); 110 | local firstSlot = M.firstAvailableSlot(slotInfo); 111 | robot.transferTo(firstSlot); 112 | int.sendSlotData(side, firstSlot); 113 | end 114 | int.sendSlotData(side, slot); 115 | end 116 | end 117 | end 118 | 119 | -- returns first slot item appears in, 120 | -- not counting the crafting grid. 121 | function M.findItem(label) 122 | for i = 1, invSize do 123 | if not craftingSlots[i] and robot.count(i) > 0 then 124 | local slotInfo = inv.getStackInInternalSlot(i); 125 | if slotInfo.label == label then 126 | return i, slotInfo.size; 127 | end 128 | end 129 | end 130 | return false, 0; 131 | end 132 | 133 | -- move one item per call 134 | function M.moveItemToSlot(label, targetSlot, amount) 135 | local slot = M.findItem(label); 136 | if slot then 137 | robot.select(slot); 138 | local side = -1; 139 | local result = robot.transferTo(targetSlot, amount); 140 | int.sendInventoryMetadata(side); 141 | int.sendSlotData(side, slot); 142 | int.sendSlotData(side, targetSlot); 143 | return result; 144 | end 145 | return false; 146 | end 147 | 148 | function M.convertGridToSlot(slot) 149 | if slot > 9 then return 0; end 150 | local grid = {[1] = 1, [2] = 2, [3] = 3, [4] = 5, [5] = 6, [6] = 7, [7] = 9, [8] = 10, [9] = 11}; 151 | return grid[slot]; 152 | end 153 | function M.convertSlotToGrid(slot) 154 | if slot > 11 then return 0; end 155 | local grid = {[1] = 1, [2] = 2, [3] = 3, [5] = 4, [6] = 5, [7] = 6, [9] = 7, [10] = 8, [11] = 9}; 156 | return grid[slot]; 157 | end 158 | 159 | function M.deepCraft(mainLabel, previousLabels) 160 | 161 | -- don't get stuck in recipe loops 162 | if previousLabels[mainLabel] then 163 | print("Already attempted to craft " .. mainLabel); 164 | return false; 165 | end 166 | previousLabels[mainLabel] = true; 167 | 168 | local recipes = M.getRecipes(mainLabel); 169 | 170 | -- make sure we have a recipe for this item 171 | if #recipes == 0 then 172 | print("No recipes for " .. mainLabel); 173 | return false; 174 | end 175 | 176 | 177 | local pattern; 178 | local allPartsCraftSuccess = false; 179 | for recipeIndex = 1, #recipes do 180 | if not allPartsCraftSuccess then 181 | local recipeChoiceCount = M.getRecipeChoiceCount(recipes[recipeIndex]); 182 | for recipeChoice = 1, recipeChoiceCount do 183 | -- if no previous attempt has succeeded 184 | if not allPartsCraftSuccess then 185 | pattern = M.getPattern(recipeChoice, recipes[recipeIndex]); 186 | local partCraftSuccess = true; 187 | for i, partLabel in pairs(pattern) do 188 | -- if all parts have been successfully crafted so far 189 | if partCraftSuccess then 190 | if partLabel then 191 | 192 | while not M.enough(partLabel, pattern) and partCraftSuccess do 193 | partCraftSuccess = M.deepCraft(partLabel, copyTable(previousLabels)); 194 | end 195 | 196 | end 197 | end 198 | end 199 | if partCraftSuccess then 200 | allPartsCraftSuccess = true; 201 | end 202 | end 203 | end 204 | end 205 | end 206 | 207 | local patternCraftSuccess = false; 208 | if allPartsCraftSuccess then 209 | patternCraftSuccess = M.craftPattern(pattern, mainLabel); 210 | if not patternCraftSuccess then 211 | print("Failed to craft " .. mainLabel); 212 | else 213 | print("Crafted " .. mainLabel); 214 | end 215 | else 216 | print("Not all parts successfully crafted"); 217 | end 218 | 219 | return patternCraftSuccess; 220 | 221 | end 222 | 223 | function M.craftPattern(pattern, mainLabel) 224 | local craftSuccess; 225 | 226 | if M.allPatternPartsPresent(pattern) then 227 | -- move the parts to the appropriate slots 228 | for i, partLabel in pairs(pattern) do 229 | M.moveItemToSlot(partLabel, M.convertGridToSlot(i), 1); 230 | end 231 | robot.select(1); 232 | craftSuccess = craft(1); 233 | M.clearCraftingGrid(); 234 | else 235 | -- iterate again to make sure we get all the parts 236 | -- not super elegant but easy to write 237 | craftSuccess = M.craft(mainLabel); 238 | end 239 | 240 | return craftSuccess; 241 | end 242 | 243 | function M.craft(itemName) 244 | M.clearCraftingGrid(); 245 | local result = M.deepCraft(itemName, {}); 246 | local side = -1; 247 | int.sendInventoryMetadata(side); 248 | for k, slotNum in pairs(craftingSlots) do 249 | if slotNum then 250 | int.sendSlotData(side, slotNum); 251 | end 252 | end 253 | return result; 254 | end 255 | 256 | function M.enough(partLabel, pattern) 257 | return M.countInInventory(partLabel) >= M.countInPattern(partLabel, pattern); 258 | end 259 | 260 | function M.allPatternPartsPresent(pattern) 261 | local allPartsPresent = true; 262 | 263 | for i, partLabel in pairs(pattern) do 264 | if partLabel then 265 | if not allPartsPresent or not M.enough(partLabel, pattern) then 266 | allPartsPresent = false; 267 | end 268 | end 269 | end 270 | 271 | return allPartsPresent; 272 | end 273 | 274 | function copyTable(table1) 275 | local table2 = {}; 276 | for key, value in pairs(table1) do 277 | table2[key] = value; 278 | end 279 | return table2; 280 | end 281 | 282 | return M; -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Roboserver! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 33 | 34 | 58 | 59 |
60 | 61 |
62 |
63 | 64 |
65 | Version: 66 | 67 |
68 | 69 |
70 | User: 71 | <%= user.username %> 72 |
73 | 74 |
75 | Position: 76 | 77 |
78 | 79 | 83 | 84 |
85 |
86 | 87 | 91 | 92 |
93 | 94 |
95 | 96 |
97 | 98 |
99 | 100 | 101 | 102 |
103 | 104 | Robot 105 | 106 | 109 | 110 |
111 | 112 | 113 |
114 | 115 | 116 |
117 | 118 |
Tools
119 | 120 |
121 |
122 |
123 | 124 | 129 | 130 | 131 | 136 | 137 | 138 | 143 | 144 |
145 | 146 |
147 | 148 | 149 | 154 | 155 | 156 | 161 | 162 |
163 | 164 |
165 | 166 |
167 | 168 |
169 | 170 | 171 | 172 |
173 | 174 |
175 | 176 | 177 | 178 |
179 | 180 |
181 | 182 |
183 | 184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |
193 |
194 | 195 |
196 | 197 |
198 | 199 |
Actions
200 | 201 |
202 |
203 | 204 |
Inventory
205 |
Scan
206 | 207 |
208 | 209 |
210 | 211 |
Equip
212 |
Center
213 | 214 |
215 |
216 | 217 |
218 | 219 | 220 |
221 | 222 |
Cutaway point
223 | 224 |
225 |
226 |
227 | 228 | 229 |
230 | 231 |
232 |
233 | 234 |
235 | 236 |
237 | 238 | Scan Size 239 | 240 | 245 | 246 |
247 | 248 |
249 | 250 |
251 |
Command History
252 |
253 | 254 | 255 |
256 |
257 |
258 | 259 |
260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | -------------------------------------------------------------------------------- /public/js/client/InventoryRender.mjs: -------------------------------------------------------------------------------- 1 | import {GUI} from '/js/client/GUI.mjs'; 2 | 3 | /** 4 | * Used to display and track inventories. 5 | */ 6 | export class InventoryRender { 7 | 8 | /** 9 | * Used to display and track inventories. 10 | * @param {object} inventoryData 11 | * @param {GUI} GUI 12 | */ 13 | constructor(inventoryData, GUI) { 14 | this.inventory = inventoryData; 15 | this.inventory.contents = {}; 16 | this.GUI = GUI; 17 | this.table = this.renderInventory(inventoryData); 18 | } 19 | 20 | /** 21 | * Tells you about the item in the given slot. First item is at 1, just like in game. 22 | * @param {number} slotNum 23 | * @returns {object} 24 | */ 25 | getSlot(slotNum) { 26 | return this.inventory.contents[slotNum - 1]; 27 | } 28 | 29 | /** 30 | * Change what's in a slot. First item is at 1, just like in game. 31 | * @param {number} slotNum 32 | * @param {object} slotData 33 | * @returns {object} 34 | */ 35 | setSlot(slotNum, slotData) { 36 | this.inventory.contents[slotNum - 1] = slotData; 37 | var slotCell = this.table.rows[Math.trunc((slotNum - 1) / 4) + 1].cells[(slotNum - 1) % 4]; 38 | if (slotCell.firstChild) {slotCell.firstChild.remove();} 39 | if (slotData && slotData.label) { 40 | slotCell.appendChild(InventoryRender.renderItem(slotData, this.GUI)); 41 | } 42 | return slotData; 43 | } 44 | 45 | /** 46 | * Gets the side this inventory is on. Used to tell if it's internal or external. 47 | * @returns {number} 48 | */ 49 | getSide() { 50 | return this.inventory.side; 51 | } 52 | 53 | /** 54 | * Adds the table to an existing element to be displayed. 55 | * @param {HTMLElement} display 56 | */ 57 | addToDisplay(display) { 58 | display.appendChild(this.table); 59 | } 60 | 61 | /** 62 | * Removes the table from its parent display element. 63 | */ 64 | removeFromDisplay() { 65 | this.table.remove(); 66 | } 67 | 68 | /** 69 | * Creates an interactive visual representation of an inventory. 70 | * @param {object} inventoryData 71 | * @returns {HTMLTableElement} 72 | * @param {GUI} GUI 73 | */ 74 | renderInventory(inventoryData) { 75 | var table = document.createElement('table'); 76 | table.classList.add('mc-table'); 77 | table.setAttribute('data-side', inventoryData.side); 78 | var numCols = 4; 79 | var numRows = inventoryData.size / 4; 80 | for (var i = 0; i < numRows; i++) { 81 | var row = table.insertRow(-1); 82 | for (var j = 0; j < numCols; j++) { 83 | if ((i * 4) + j < inventoryData.size) { 84 | var cell = row.insertCell(-1); 85 | cell.classList.add('mc-td'); 86 | if (inventoryData.side == -1) { 87 | cell.addEventListener('click', this.changeSelectedSlot.bind(this.GUI)); 88 | } 89 | 90 | cell.addEventListener('dragover', this.allowDrop); 91 | cell.addEventListener('drop', this.itemDrop.bind(this.GUI)); 92 | 93 | var slotNumber = (i * numCols) + j + 1; 94 | cell.setAttribute('data-slotNumber', slotNumber); 95 | if (inventoryData.selected == slotNumber) { 96 | cell.setAttribute('data-selected', true); 97 | } 98 | } 99 | } 100 | } 101 | 102 | var inventoryLabel = document.createElement('span'); 103 | var inventoryType = inventoryData.side == -1 ? 'Robot' : 'External'; 104 | inventoryLabel.innerText = inventoryType + ' Inventory'; 105 | 106 | var header = table.createTHead(); 107 | var headerRow = document.createElement('tr'); 108 | var headerCell = document.createElement('th'); 109 | header.appendChild(headerRow); 110 | headerRow.appendChild(headerCell); 111 | headerCell.appendChild(inventoryLabel); 112 | 113 | return table; 114 | } 115 | 116 | /** 117 | * Creates a visual representation of an item. 118 | * @param {object} itemData 119 | * @param {GUI} guiInstance 120 | * @returns {HTMLDivElement} 121 | */ 122 | static renderItem(itemData, guiInstance) { 123 | let itemDiv = document.createElement('div'); 124 | let title = `${itemData.label}`; 125 | itemDiv.setAttribute('title', title); 126 | itemDiv.setAttribute('style', "text-align: center;"); 127 | GUI.addToolTip(itemDiv, title); 128 | 129 | itemDiv.addEventListener('dragstart', InventoryRender.itemDragStart); 130 | itemDiv.setAttribute('draggable', true); 131 | 132 | let svgCube; 133 | let shortName = itemData.name.replace('minecraft:', ''); 134 | if (shortName in guiInstance.namesToHardness && !guiInstance.game.mapRender.simple) { 135 | 136 | let hardness = guiInstance.namesToHardness[shortName]; 137 | let colorNumber = guiInstance.game.mapRender.hardnessToColorData[hardness]; 138 | 139 | function darken(colorNumber) { 140 | let percent = -.15; 141 | let t=percent<0?0:255,p=percent<0?percent*-1:percent,R=colorNumber>>16,G=colorNumber>>8&0x00FF,B=colorNumber&0x0000FF; 142 | return parseInt((0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1), 16); 143 | } 144 | 145 | function colorNumberToString(colorNumber) { 146 | return `#${colorNumber.toString(16)}`; 147 | } 148 | 149 | svgCube = InventoryRender.makeCubeSVG( 150 | colorNumberToString(colorNumber), 151 | colorNumberToString(darken(colorNumber)), 152 | colorNumberToString(darken(darken(colorNumber))), 153 | ); 154 | 155 | } 156 | else { 157 | svgCube = InventoryRender.makeCubeSVG('white', 'gray', 'black') 158 | } 159 | itemDiv.appendChild(svgCube); 160 | 161 | let numberDiv = document.createElement('div'); 162 | numberDiv.classList.add('itemStackNumber'); 163 | numberDiv.innerText = itemData.size; 164 | itemDiv.appendChild(numberDiv); 165 | 166 | itemDiv.itemData = itemData; 167 | 168 | return itemDiv; 169 | } 170 | 171 | /** 172 | * Stores item data for transfer. 173 | * @param {Event} e 174 | */ 175 | static itemDragStart(e) { 176 | let cell = e.target; 177 | $(cell).tooltip('hide'); 178 | GLOBALS.dragStartElement = cell.parentElement; 179 | if (e.ctrlKey || e.altKey) { 180 | e.dataTransfer.setData('text/plain', 'split'); 181 | } 182 | else { 183 | e.dataTransfer.setData('text/plain', 'move'); 184 | } 185 | } 186 | 187 | /** 188 | * Transfers item data. 189 | * @param {Event} e 190 | */ 191 | itemDrop(e) { 192 | e.preventDefault(); 193 | let cell = e.target; 194 | while (cell.tagName != "TD") {cell = cell.parentElement;} 195 | if (GLOBALS.dragStartElement != cell) { 196 | let operation = e.dataTransfer.getData('text'); 197 | if (operation == 'move') { 198 | InventoryRender.validateTransfer(GLOBALS.dragStartElement, cell, undefined, this); 199 | } 200 | else if (operation == 'split') { 201 | $('#itemTransferAmountModal').modal('show'); 202 | GLOBALS.inProgressTransfer = {}; 203 | GLOBALS.inProgressTransfer.start = GLOBALS.dragStartElement; 204 | GLOBALS.inProgressTransfer.end = cell; 205 | this.transferAmountInput.focus(); 206 | } 207 | } 208 | return false; 209 | } 210 | 211 | /** 212 | * Allows table cells to receive drops. 213 | * @param {Event} e 214 | */ 215 | allowDrop(e) { 216 | e.preventDefault(); 217 | return false; 218 | } 219 | 220 | /** 221 | * Changes which inventory slot is selected. 222 | * @param {Event} e 223 | */ 224 | changeSelectedSlot(e) { 225 | let cell = e.target; 226 | while (cell.tagName != "TD") {cell = cell.parentElement;} 227 | cell.parentElement.parentElement.querySelector('[data-selected=true]').removeAttribute('data-selected'); 228 | cell.setAttribute('data-selected', true); 229 | var commandName = 'select'; 230 | var commandParameters = [parseInt(cell.getAttribute('data-slotnumber'))]; 231 | this.sendCommand(commandName, commandParameters); 232 | } 233 | 234 | /** 235 | * Ensures transfers initiated on the client side are possible before attempting to execute them. 236 | * @param {HTMLTableCellElement} fromCell 237 | * @param {HTMLTableCellElement} toCell 238 | * @param {number} amount 239 | * @param {GUI} GUI 240 | */ 241 | static validateTransfer(fromCell, toCell, amount, GUI) { 242 | amount = parseInt(amount); 243 | var success = false; 244 | 245 | if (!fromCell.firstChild || 246 | InventoryRender.getSide(fromCell) !== -1 && InventoryRender.getSide(toCell) !== -1) {;} 247 | else { 248 | var data1 = fromCell.firstChild.itemData; 249 | if (amount > data1.size || amount < 1) {;} 250 | else if (!toCell.firstChild) { 251 | InventoryRender.transferAndUpdate(fromCell, toCell, amount || data1.size, GUI); 252 | success = true; 253 | } 254 | else { 255 | var data2 = toCell.firstChild.itemData; 256 | if (data1.name == data2.name && 257 | !data1.damage && !data2.damage && 258 | !data1.hasTag && !data2.hasTag) { 259 | var data2Space = data2.maxSize - data2.size; 260 | if (amount) { 261 | var amountToTransfer = Math.min(amount, data2Space); 262 | } 263 | else { 264 | var amountToTransfer = Math.min(data1.size, data2Space); 265 | } 266 | InventoryRender.transferAndUpdate(fromCell, toCell, amountToTransfer, GUI); 267 | success = true; 268 | } 269 | else { 270 | if (!amount) { 271 | InventoryRender.swapCells(fromCell, toCell, GUI); 272 | } 273 | } 274 | } 275 | } 276 | 277 | return success; 278 | } 279 | 280 | /** 281 | * Tells you which inventory a cell is in. 282 | * @param {HTMLTableCellElement} cell 283 | */ 284 | static getSide(cell) { 285 | return parseInt(cell.parentElement.parentElement.parentElement.getAttribute('data-side')); 286 | } 287 | 288 | /** 289 | * Moves items from one slot to another. 290 | * @param {HTMLTableCellElement} fromCell 291 | * @param {HTMLTableCellElement} toCell 292 | * @param {number} amount 293 | * @param {GUI} GUI 294 | */ 295 | static transferAndUpdate(fromCell, toCell, amount, GUI) { 296 | amount = parseInt(amount); 297 | if (amount) { 298 | var data1 = fromCell.firstChild.itemData; 299 | data1.size -= amount; 300 | fromCell.removeChild(fromCell.firstChild); 301 | if (data1.size) {fromCell.appendChild(InventoryRender.renderItem(data1, GUI));} 302 | if (toCell.firstChild) { 303 | var data2 = toCell.firstChild.itemData; 304 | data2.size += amount; 305 | toCell.removeChild(toCell.firstChild); 306 | } 307 | else { 308 | var data2 = Object.assign({}, data1); 309 | data2.size = amount; 310 | } 311 | let newItem = InventoryRender.renderItem(data2, GUI); 312 | toCell.appendChild(newItem); 313 | $(newItem).tooltip('destroy'); 314 | 315 | var commandParameters = [ 316 | parseInt(fromCell.getAttribute('data-slotnumber')), 317 | InventoryRender.getSide(fromCell), 318 | parseInt(toCell.getAttribute('data-slotnumber')), 319 | InventoryRender.getSide(toCell), 320 | amount, 321 | ]; 322 | var commandName = 'transfer'; 323 | GUI.sendCommand(commandName, commandParameters); 324 | } 325 | } 326 | 327 | /** 328 | * Exchanges the children of two table cells. Used to swap item slots. 329 | * @param {HTMLTableCellElement} cell1 330 | * @param {HTMLTableCellElement} cell2 331 | * @param {GUI} GUI 332 | */ 333 | static swapCells(cell1, cell2, GUI) { 334 | if (cell1.firstChild) { 335 | let amount = cell1.firstChild.itemData.size; 336 | let itemSwapStorage = cell1.firstChild; 337 | cell1.removeChild(cell1.firstChild); 338 | if (cell2.firstChild) {cell1.appendChild(cell2.firstChild);} 339 | if (itemSwapStorage) {cell2.appendChild(itemSwapStorage);} 340 | 341 | var commandParameters = [ 342 | parseInt(cell1.getAttribute('data-slotnumber')), 343 | InventoryRender.getSide(cell1), 344 | parseInt(cell2.getAttribute('data-slotnumber')), 345 | InventoryRender.getSide(cell2), 346 | amount, 347 | ]; 348 | var commandName = 'transfer'; 349 | GUI.sendCommand(commandName, commandParameters); 350 | } 351 | } 352 | 353 | /** 354 | * Used to create cube images to represent items. 355 | * @param {string} topColor 356 | * @param {string} leftColor 357 | * @param {string} rightColor 358 | */ 359 | static makeCubeSVG(topColor, leftColor, rightColor) { 360 | 361 | let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 362 | svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); 363 | svg.setAttribute('viewBox', '0 0 1732 2000'); 364 | svg.setAttribute('style', 'height: inherit;'); 365 | 366 | let top = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 367 | top.setAttribute('points', '0,500 866,0 1732,500 866,1000'); 368 | top.style.fill = topColor; 369 | 370 | let left = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 371 | left.setAttribute('points', '0,500 867,1000 867,2000 0,1500'); 372 | left.style.fill = leftColor; 373 | 374 | let right = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 375 | right.setAttribute('points', '1732,500 866,999 866,2000 1732,1500'); 376 | right.style.fill = rightColor; 377 | 378 | svg.appendChild(top); 379 | svg.appendChild(left); 380 | svg.appendChild(right); 381 | 382 | return svg; 383 | 384 | } 385 | 386 | } --------------------------------------------------------------------------------