├── .gitignore ├── .gitattributes ├── client ├── icon.png ├── favicon.png ├── js │ ├── events │ │ ├── settings.js │ │ ├── arm_animation.js │ │ ├── look.js │ │ ├── block_dig.js │ │ ├── position_look.js │ │ ├── index.js │ │ ├── block_place.js │ │ ├── position.js │ │ └── chat.js │ ├── commands │ │ ├── ping.js │ │ ├── motd.js │ │ ├── index.js │ │ ├── tp.js │ │ └── gamemode.js │ ├── generators │ │ ├── README.md │ │ ├── grass.js │ │ ├── index.js │ │ ├── all_the_blocks.js │ │ ├── superflat.js │ │ ├── nether.js │ │ └── minecraft.js │ ├── storage.js │ ├── event.js │ ├── plugins │ │ └── inventory.js │ ├── command.js │ ├── main.js │ ├── world.js │ └── mc-server.js ├── index.html └── css │ └── style.css ├── TODO.txt ├── webpack.config.js ├── .vscode └── settings.json ├── LICENSE ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | anvil_*/ 3 | node_modules/ 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /client/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vantezzen/cauldron-js/HEAD/client/icon.png -------------------------------------------------------------------------------- /client/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vantezzen/cauldron-js/HEAD/client/favicon.png -------------------------------------------------------------------------------- /client/js/events/settings.js: -------------------------------------------------------------------------------- 1 | export const event = 'settings' 2 | 3 | export const handle = (event, data, metadata, client, clientIndex, server) => { 4 | server.clientSettings[client.id] = data 5 | } 6 | -------------------------------------------------------------------------------- /client/js/commands/ping.js: -------------------------------------------------------------------------------- 1 | export const command = 'ping' 2 | export const info = 'Ping - Pong' 3 | export const usage = '/ping' 4 | export const handle = (command, components, client, clientIndex, server) => { 5 | server.sendMessage(client.id, 'Pong') 6 | } 7 | -------------------------------------------------------------------------------- /client/js/events/arm_animation.js: -------------------------------------------------------------------------------- 1 | export const event = 'arm_animation' 2 | export const handle = (event, data, metadata, client, clientIndex, server) => { 3 | server.writeOthers('animation', { 4 | entityId: client.id, 5 | animation: 0 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | PHASE 1, essentials: 2 | - ✓ 3 | 4 | PHASE 2, making it playable: 5 | - Improve "minecraft" world generator 6 | - Implement survival (decrement item counter etc.) 7 | - Implement crafting 8 | - Implement chests 9 | - Implement mobs 10 | 11 | PHASE 3, nice to have: 12 | - Export world 13 | - Implement sounds 14 | - Add support for plugins -------------------------------------------------------------------------------- /client/js/events/look.js: -------------------------------------------------------------------------------- 1 | export const event = 'look' 2 | export const handle = (event, data, metadata, client, clientIndex, server) => { 3 | server.db.players.update(client.uuid, { 4 | yaw: data.yaw, 5 | pitch: data.pitch 6 | }) 7 | server.writeOthers(client.id, 'entity_look', { 8 | entityId: client.id, 9 | yaw: server.conv(data.yaw), 10 | pitch: server.conv(data.pitch) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /client/js/generators/README.md: -------------------------------------------------------------------------------- 1 | # Cauldron.JS World generators 2 | 3 | This folder contains all world generators for CauldronJS. 4 | 5 | The world generators `all_the_blocks`, `grass`, `nether` and `superflat` are customized versions of the world genertors found inside [flying-squid](https://github.com/PrismarineJS/flying-squid/blob/master/src/lib/worldGenerations). 6 | 7 | `minecraft` is a world generator that tries to generate a vanilla minecraft-like world. -------------------------------------------------------------------------------- /client/js/commands/motd.js: -------------------------------------------------------------------------------- 1 | export const command = 'motd' 2 | export const info = 'Change the MOTD of your server' 3 | export const usage = '/motd [Text]' 4 | export const handle = (command, components, client, clientIndex, server) => { 5 | if (components.length < 1) { 6 | server.sendMessage(client.id, 'Usage: ' + usage) 7 | return 8 | } 9 | 10 | const motd = components.join(' ') 11 | 12 | console.log(motd) 13 | 14 | server.socket.emit('motd', motd) 15 | server.sendMessage(client.id, 'MOTD updated') 16 | } 17 | -------------------------------------------------------------------------------- /client/js/generators/grass.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | import ChunkGen from 'prismarine-chunk' 3 | 4 | export default function generation ({ version }) { 5 | const Chunk = ChunkGen(version) 6 | 7 | function generateChunk () { 8 | const chunk = new Chunk() 9 | 10 | for (let x = 0; x < 16; x++) { 11 | for (let z = 0; z < 16; z++) { 12 | chunk.setBlockType(new Vec3(x, 50, z), 2) 13 | for (let y = 0; y < 256; y++) { 14 | chunk.setSkyLight(new Vec3(x, y, z), 15) 15 | } 16 | } 17 | } 18 | 19 | return chunk 20 | } 21 | return generateChunk 22 | } 23 | -------------------------------------------------------------------------------- /client/js/commands/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * commands/index.js - Combining command handlers 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import * as ping from './ping' 12 | import * as tp from './tp' 13 | import * as motd from './motd' 14 | import * as gamemode from './gamemode' 15 | 16 | export default [ 17 | ping, 18 | tp, 19 | motd, 20 | gamemode 21 | ] 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * Configuration for webpack for the frontend 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | const path = require('path') 12 | 13 | module.exports = { 14 | entry: ['./client/js/main'], 15 | mode: 'development', 16 | output: { 17 | path: path.join(__dirname, 'client/dist'), 18 | filename: 'bundle.js' 19 | }, 20 | devtool: 'source-map' 21 | } -------------------------------------------------------------------------------- /client/js/commands/tp.js: -------------------------------------------------------------------------------- 1 | export const command = 'tp' 2 | export const info = 'Teleport yourself' 3 | export const usage = '/tp [x] [y] [z]' 4 | export const handle = (command, components, client, clientIndex, server) => { 5 | if (components.length !== 3) { 6 | server.sendMessage(client.id, 'Usage: ' + usage) 7 | return 8 | } 9 | 10 | const [x, y, z] = components 11 | 12 | server.write(client.id, 'position', { 13 | x, 14 | y, 15 | z, 16 | yaw: 0, 17 | pitch: 0, 18 | flags: 0x00 19 | }) 20 | server.writeOthers(client.id, 'entity_teleport', { 21 | entityId: client.id, 22 | x, 23 | y, 24 | z, 25 | onGround: false 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /client/js/generators/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * World generators 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import all_the_blocks from './all_the_blocks' // eslint-disable-line 12 | import grass from './grass' 13 | import nether from './nether' 14 | import superflat from './superflat' 15 | import minecraft from './minecraft' 16 | 17 | export default { 18 | all_the_blocks, 19 | grass, 20 | nether, 21 | superflat, 22 | minecraft 23 | } 24 | -------------------------------------------------------------------------------- /client/js/events/block_dig.js: -------------------------------------------------------------------------------- 1 | // import uuid from 'uuid' 2 | 3 | export const event = 'block_dig' 4 | export const handle = (event, data, metadata, client, clientIndex, server) => { 5 | if (client.gameMode === 1 || data.status === 2) { 6 | // Get block that has been dug 7 | server.world.getBlock(data.location.x, data.location.y, data.location.z) 8 | .then(block => { 9 | if (block) { 10 | // TODO: Spawn object 11 | } 12 | }) 13 | 14 | server.writeAll('block_change', { 15 | location: data.location, 16 | type: 0 17 | }) 18 | server.world.setBlock(data.location.x, data.location.y, data.location.z, { 19 | type: 0 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#65c89b", 4 | "activityBar.foreground": "#15202b", 5 | "activityBar.inactiveForeground": "#15202b99", 6 | "activityBarBadge.background": "#945bc4", 7 | "activityBarBadge.foreground": "#e7e7e7", 8 | "titleBar.activeBackground": "#42b883", 9 | "titleBar.inactiveBackground": "#42b88399", 10 | "titleBar.activeForeground": "#15202b", 11 | "titleBar.inactiveForeground": "#15202b99", 12 | "statusBar.background": "#42b883", 13 | "statusBarItem.hoverBackground": "#359268", 14 | "statusBar.foreground": "#15202b" 15 | }, 16 | "peacock.color": "#42b883" 17 | } -------------------------------------------------------------------------------- /client/js/storage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * storage.js - Manage dexie storage 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import Dexie from 'dexie' 12 | import Debugger from 'debug' 13 | const debug = Debugger('cauldron:storage') 14 | 15 | export const openDatabase = () => { 16 | // Create database using Dexie 17 | const db = new Dexie('Minecraft') 18 | db.version(1).stores({ 19 | players: 'uuid, x, y, z, yaw, pitch, onGround' 20 | }) 21 | 22 | debug('Opened database') 23 | 24 | return db 25 | } 26 | -------------------------------------------------------------------------------- /client/js/generators/all_the_blocks.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | import ChunkGen from 'prismarine-chunk' 3 | import data from 'minecraft-data' 4 | 5 | export default function generation ({ version }) { 6 | const Chunk = ChunkGen(version) 7 | const blocks = data(version).blocks 8 | 9 | function generateSimpleChunk () { 10 | const chunk = new Chunk() 11 | 12 | let i = 2 13 | for (let x = 0; x < 16; x++) { 14 | for (let z = 0; z < 16; z++) { 15 | let y 16 | for (y = 47; y <= 50; y++) { 17 | chunk.setBlockType(new Vec3(x, y, z), i) 18 | i = (i + 1) % Object.keys(blocks).length 19 | } 20 | for (y = 0; y < 256; y++) { 21 | chunk.setSkyLight(new Vec3(x, y, z), 15) 22 | } 23 | } 24 | } 25 | return chunk 26 | } 27 | return generateSimpleChunk 28 | } 29 | -------------------------------------------------------------------------------- /client/js/events/position_look.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | 3 | export const event = 'position_look' 4 | export const handle = (event, data, metadata, client, clientIndex, server) => { 5 | const update = { 6 | x: data.x, 7 | y: data.y, 8 | z: data.z, 9 | yaw: data.yaw, 10 | pitch: data.pitch, 11 | onGround: data.onGround 12 | } 13 | server.db.players.update(client.uuid, update) 14 | server.clients[clientIndex].position = { 15 | ...server.clients[clientIndex].position, 16 | ...update 17 | } 18 | 19 | const position = new Vec3(data.x, data.y, data.z).scaled(32).floored() 20 | server.writeOthers(client.id, 'entity_teleport', { 21 | entityId: client.id, 22 | x: position.x, 23 | y: position.y, 24 | z: position.z, 25 | yaw: server.conv(data.yaw), 26 | pitch: server.conv(data.pitch), 27 | onGround: data.onGround 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /client/js/events/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * events/index.js - Combining event handlers 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import * as armAnimation from './arm_animation' 12 | import * as look from './look' 13 | import * as positionLook from './position_look' 14 | import * as position from './position' 15 | import * as blockDig from './block_dig' 16 | import * as blockPlace from './block_place' 17 | import * as chat from './chat' 18 | import * as settings from './settings' 19 | 20 | export default [ 21 | armAnimation, 22 | look, 23 | positionLook, 24 | position, 25 | blockDig, 26 | blockPlace, 27 | chat, 28 | settings 29 | ] 30 | -------------------------------------------------------------------------------- /client/js/events/block_place.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | 3 | export const event = 'block_place' 4 | export const handle = (event, data, metadata, client, clientIndex, server) => { 5 | if ((data.location.x === -1 && data.location.y === -1 && data.location.z === -1) || data.heldItem.blockId === -1) { 6 | // Invalid block placement 7 | return 8 | } 9 | const directionToVector = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)] 10 | const referencePosition = new Vec3(data.location.x, data.location.y, data.location.z) 11 | const directionVector = directionToVector[data.direction] 12 | const placedPosition = referencePosition.plus(directionVector) 13 | 14 | server.writeAll('block_change', { 15 | location: placedPosition, 16 | type: data.heldItem.blockId << 4 | data.heldItem.itemDamage 17 | }) 18 | 19 | server.world.setBlock(placedPosition.x, placedPosition.y, placedPosition.z, { 20 | type: data.heldItem.blockId, 21 | metadata: data.heldItem.itemDamage 22 | }) 23 | // data.heldItem.blockId << 4 | data.heldItem.itemDamage 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 vantezzen 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. 22 | -------------------------------------------------------------------------------- /client/js/commands/gamemode.js: -------------------------------------------------------------------------------- 1 | export const command = 'gamemode' 2 | export const info = 'Update your gamemode' 3 | export const usage = '/gamemode [0|1|2|3|survival|creative|adventure|spectator]' 4 | export const handle = (command, components, client, clientIndex, server) => { 5 | if (components.length !== 1) { 6 | server.sendMessage(client.id, 'Usage: /gamemode [gamemode]') 7 | return 8 | } 9 | 10 | let [gameMode] = components 11 | 12 | if (gameMode != Number(gameMode)) { // eslint-disable-line 13 | if (gameMode === 'survival') { 14 | gameMode = 0 15 | } else if (gameMode === 'creative') { 16 | gameMode = 1 17 | } else if (gameMode === 'adventure') { 18 | gameMode = 2 19 | } else if (gameMode === 'spectator') { 20 | gameMode = 3 21 | } else { 22 | server.sendMessage(client.id, 'Invalid gamemode') 23 | return 24 | } 25 | } else if (gameMode > 3) { 26 | server.sendMessage(client.id, 'Invalid gamemode') 27 | return 28 | } 29 | 30 | server.clients[clientIndex].gameMode = gameMode 31 | 32 | server.write(client.id, 'game_state_change', { 33 | reason: 3, 34 | gameMode 35 | }) 36 | server.sendMessage(client.id, 'Gamemode updated') 37 | } 38 | -------------------------------------------------------------------------------- /client/js/generators/superflat.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | import ChunkGen from 'prismarine-chunk' 3 | 4 | export default function generation ( 5 | version, 6 | bottomId = 7, 7 | middleId = 1, 8 | topId = 2, 9 | middleThickness = 3, 10 | debug = false 11 | ) { 12 | const Chunk = ChunkGen(version) 13 | 14 | function generateChunk () { 15 | const chunk = new Chunk() 16 | const height = middleThickness + 1 17 | const DEBUG_POINTS = [new Vec3(0, height, 0), new Vec3(15, height, 0), new Vec3(0, height, 15), new Vec3(15, height, 15)] 18 | for (let x = 0; x < 16; x++) { 19 | for (let z = 0; z < 16; z++) { 20 | for (let y = 0; y < middleThickness + 2; y++) { 21 | if (y === 0) chunk.setBlockType(new Vec3(x, y, z), bottomId) 22 | else if (y < middleThickness + 1) chunk.setBlockType(new Vec3(x, y, z), middleId) 23 | else chunk.setBlockType(new Vec3(x, y, z), topId) 24 | } 25 | for (let y = 0; y < 256; y++) { 26 | chunk.setSkyLight(new Vec3(x, y, z), 15) 27 | } 28 | } 29 | } 30 | 31 | if (debug) { 32 | DEBUG_POINTS.forEach(p => chunk.setBlockType(p, 35)) 33 | } 34 | return chunk 35 | } 36 | return generateChunk 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cauldron-js", 3 | "version": "0.1.0", 4 | "description": "Minecraft Server in your browser", 5 | "main": "index.js", 6 | "author": "vantezzen", 7 | "license": "MIT", 8 | "dependencies": { 9 | "chalk": "^2.4.2", 10 | "debug": "^4.1.1", 11 | "dexie": "^2.0.4", 12 | "express": "^4.17.1", 13 | "ip": "^1.1.5", 14 | "localforage": "^1.7.3", 15 | "minecraft-data": "^2.39.0", 16 | "minecraft-protocol": "^1.9.3", 17 | "noisejs": "^2.1.0", 18 | "prismarine-chunk": "^1.11.0", 19 | "prismarine-item": "^1.1.0", 20 | "prismarine-nbt": "^1.2.1", 21 | "prismarine-windows": "^1.1.0", 22 | "random-seed": "^0.3.0", 23 | "socket.io": "^2.2.0", 24 | "socket.io-client": "^2.2.0", 25 | "spiralloop": "^1.0.2", 26 | "uint4": "^0.1.2", 27 | "uuid": "^3.3.2", 28 | "vec3": "^0.1.3" 29 | }, 30 | "scripts": { 31 | "start": "node index.js", 32 | "dev": "DEBUG=cauldron:* LOCATION=127.0.0.1 yarn start", 33 | "watch": "webpack --watch", 34 | "lint": "standard --fix \"client/js/**/*.js\" \"index.js\"" 35 | }, 36 | "devDependencies": { 37 | "standard": "^13.1.0", 38 | "webpack": "^4.38.0", 39 | "webpack-cli": "^3.3.6" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/js/generators/nether.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | import ChunkGen from 'prismarine-chunk' 3 | import rand from 'random-seed' 4 | 5 | const seed = 'A' 6 | 7 | export default function generation ({ version }) { 8 | const Chunk = ChunkGen(version) 9 | 10 | function generateChunk (chunkX, chunkZ) { 11 | const seedRand = rand.create(seed + ':' + chunkX + ':' + chunkZ) 12 | const chunk = new Chunk() 13 | for (let x = 0; x < 16; x++) { 14 | for (let z = 0; z < 16; z++) { 15 | const bedrockheighttop = 1 + seedRand(4) 16 | const bedrockheightbottom = 1 + seedRand(4) 17 | for (let y = 0; y < 128; y++) { // Nether only goes up to 128 18 | let block 19 | let data 20 | const level = 50 21 | 22 | if (y < bedrockheightbottom) block = 7 23 | else if (y < level) block = seedRand(50) === 0 ? 89 : 87 24 | else if (y > 127 - bedrockheighttop) block = 7 25 | 26 | const pos = new Vec3(x, y, z) 27 | if (block) chunk.setBlockType(pos, block) 28 | if (data) chunk.setBlockData(pos, data) 29 | // Don't need to set light data in nether 30 | } 31 | } 32 | } 33 | 34 | return chunk 35 | } 36 | return generateChunk 37 | } 38 | -------------------------------------------------------------------------------- /client/js/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * event.js - MCEvent - Handle Minecraft Event 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import events from './events' 12 | import Debugger from 'debug' 13 | const debug = Debugger('cauldron:mc-event') 14 | 15 | export default class MCEvent { 16 | constructor () { 17 | this.events = {} 18 | 19 | // Add event handlers 20 | for (const event of events) { 21 | this.addHandler(event.event, event.handle) 22 | } 23 | 24 | debug('Constructed MCEvent for ' + Object.keys(this.events).length + ' events') 25 | } 26 | 27 | addHandler (event, handler) { 28 | if (!this.events[event]) { 29 | this.events[event] = [] 30 | } 31 | 32 | this.events[event].push(handler) 33 | } 34 | 35 | // Handle new event 36 | handle (event, data, metadata, client, clientIndex, server) { 37 | // debug('Handling new MCEvent', event, id); 38 | if (this.events[event]) { 39 | for (const handler of this.events[event]) { 40 | handler(event, data, metadata, client, clientIndex, server) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/js/plugins/inventory.js: -------------------------------------------------------------------------------- 1 | import ItemLoader from 'prismarine-item' 2 | import WindowLoader from 'prismarine-windows' 3 | 4 | export default function pluginInventory (server) { 5 | const Item = ItemLoader(server.version) 6 | const windows = WindowLoader(server.version).windows 7 | 8 | server.event.addHandler('login', (event, data, metadata, client, clientIndex, server) => { 9 | server.clients[clientIndex].heldItemSlot = 0 10 | server.clients[clientIndex].heldItem = new Item(256, 1) 11 | server.clients[clientIndex].inventory = new windows.InventoryWindow(0, 'Inventory', 44) 12 | }) 13 | 14 | server.event.addHandler('held_item_slot', (event, data, metadata, client, clientIndex, server) => { 15 | server.clients[clientIndex].heldItemSlot = data.slotId 16 | server.clients[clientIndex].heldItem = client.inventory.slots[36 + data.slotId] 17 | 18 | server.writeOthers(client.id, 'entity_equipment', { 19 | entityId: client.id, 20 | slot: 0, 21 | item: Item.toNotch(server.clients[clientIndex].heldItem) 22 | }) 23 | }) 24 | 25 | server.event.addHandler('set_creative_slot', (event, data, metadata, client, clientIndex, server) => { 26 | if (data.item.blockId === -1) { 27 | server.clients[clientIndex].inventory.updateSlot(data.slot, undefined) 28 | return 29 | } 30 | 31 | const newItem = Item.fromNotch(data.item) 32 | server.clients[clientIndex].inventory.updateSlot(data.slot, newItem) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /client/js/events/position.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | 3 | export const event = 'position' 4 | 5 | export const handle = (event, data, metadata, client, clientIndex, server) => { 6 | // Get previous position (not in use as relative change is unused) 7 | // const prev = { 8 | // ...server.clients[clientIndex].position 9 | // } 10 | 11 | // Update position in database and client list 12 | const update = { 13 | x: data.x, 14 | y: data.y, 15 | z: data.z, 16 | onGround: data.onGround 17 | } 18 | server.db.players.update(client.uuid, update) 19 | server.clients[clientIndex].position = { 20 | ...server.clients[clientIndex].position, 21 | ...update 22 | } 23 | 24 | const position = new Vec3(data.x, data.y, data.z) 25 | // const lastPosition = new Vec3(prev.x, prev.y, prev.z) 26 | // const diff = position.minus(lastPosition) 27 | 28 | // const maxDelta = 3; 29 | 30 | // if (diff.abs().x > maxDelta || diff.abs().y > maxDelta || diff.abs().z > maxDelta) { 31 | // Teleport player to new position 32 | const entityPosition = position.scaled(32).floored() 33 | server.writeOthers(client.id, 'entity_teleport', { 34 | entityId: client.id, 35 | x: entityPosition.x, 36 | y: entityPosition.y, 37 | z: entityPosition.z, 38 | onGround: data.onGround 39 | }) 40 | 41 | // Relative movement is theoratically possible but as the server is so slow, it will result 42 | // in incorrect player positions 43 | // } else if (diff.distanceTo(new Vec3(0, 0, 0)) !== 0) { 44 | // // Move player relative to current position 45 | // const delta = diff.scaled(32).floored() 46 | 47 | // server.writeOthers(client.id, 'rel_entity_move', { 48 | // entityId: client.id, 49 | // dX: delta.x, 50 | // dY: delta.y, 51 | // dZ: delta.z, 52 | // onGround: data.onGround 53 | // }) 54 | // } 55 | // Send current chunk to player 56 | server.world.sendNearbyChunks(data.x, data.z, client.id) 57 | } 58 | -------------------------------------------------------------------------------- /client/js/command.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * event.js - MCEvent - Handle Minecraft chat commands 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import commands from './commands' 12 | import Debugger from 'debug' 13 | const debug = Debugger('cauldron:mc-command') 14 | 15 | export default class MCCommand { 16 | constructor (server) { 17 | this.commands = {} 18 | this.server = server 19 | 20 | // Add command handlers 21 | for (const command of commands) { 22 | this.addCommand(command.command, command.handle, command.info, command.usage) 23 | } 24 | 25 | debug('Constructed MCCommand with ' + Object.keys(this.commands).length + ' commands') 26 | } 27 | 28 | // Add a new command handler 29 | addCommand (command, handler, info, usage) { 30 | this.commands[command] = { 31 | handler, 32 | info, 33 | usage 34 | } 35 | } 36 | 37 | // Handle new command 38 | handle (command, client, clientIndex) { 39 | const commandComponents = command.split(' ') 40 | const mainCommand = commandComponents.shift() 41 | 42 | debug('Handling new command', mainCommand) 43 | 44 | if (mainCommand === 'help') { 45 | if (commandComponents.length === 0) { 46 | this.server.sendMessage(client.id, 'Availible commands:') 47 | 48 | for (const command of Object.keys(this.commands)) { 49 | this.server.sendMessage(client.id, `/${command} - ${this.commands[command].info}`) 50 | } 51 | } else if (commandComponents.length === 1) { 52 | if (this.commands[commandComponents[0]]) { 53 | const cmd = commandComponents[0] 54 | this.server.sendMessage(client.id, `Usage: ${this.commands[cmd].usage}`) 55 | } else { 56 | this.server.sendMessage(client.id, `Unknown command "${commandComponents[0]}"`) 57 | } 58 | } else { 59 | this.server.sendMessage(client.id, 'Invalid number of arguments for help') 60 | this.server.sendMessage(client.id, 'Usage: /help ([command])') 61 | } 62 | } else if (this.commands[mainCommand]) { 63 | this.commands[mainCommand].handler(mainCommand, commandComponents, client, clientIndex, this.server) 64 | } else { 65 | this.server.sendMessage(client.id, 'Unknown command') 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/js/events/chat.js: -------------------------------------------------------------------------------- 1 | export const event = 'chat' 2 | 3 | const parseClassic = (message) => { 4 | if (typeof message === 'object') return message 5 | const messageList = [] 6 | let text = '' 7 | let nextChanged = false 8 | let color = 'white' 9 | let bold = false 10 | let italic = false 11 | let underlined = false 12 | let strikethrough = false 13 | let random = false 14 | const colors = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'k', 'l', 'm', 'n', 'o', 'r', '&'] 15 | const convertColor = ['black', 'dark_blue', 'dark_green', 'dark_cyan', 'dark_red', 'dark_purple', 'gold', 16 | 'gray', 'dark_gray', 'blue', 'green', 'aqua', 'red', 'light_purple', 'yellow', 'white', 17 | 'random', 'bold', 'strikethrough', 'underlined', 'italic', 'reset', '&' 18 | ] 19 | 20 | function createJSON () { 21 | if (!text.trim()) return 22 | messageList.push({ 23 | text: text, 24 | color: color, 25 | bold: bold, 26 | italic: italic, 27 | underlined: underlined, 28 | strikethrough: strikethrough, 29 | obfuscated: random 30 | }) 31 | text = '' 32 | } 33 | 34 | while (message !== '') { 35 | const currChar = message[0] 36 | if (nextChanged) { 37 | const newColor = convertColor[colors.indexOf(currChar)] 38 | if (newColor) { 39 | if (newColor === 'bold') bold = true 40 | else if (newColor === 'strikethrough') strikethrough = true 41 | else if (newColor === 'underlined') underlined = true 42 | else if (newColor === 'italic') italic = true 43 | else if (newColor === 'random') random = true 44 | else if (newColor === '&') text += '&' 45 | else if (newColor === 'reset') { 46 | strikethrough = false 47 | bold = false 48 | underlined = false 49 | random = false 50 | italic = false 51 | color = 'white' 52 | } else color = newColor 53 | } 54 | nextChanged = false 55 | } else if (currChar === '&') { 56 | if (nextChanged) { 57 | text += '&' 58 | nextChanged = false 59 | } else { 60 | nextChanged = true 61 | createJSON() 62 | } 63 | } else { 64 | text += currChar 65 | } 66 | 67 | message = message.slice(1, message.length) 68 | } 69 | createJSON() 70 | 71 | if (messageList.length > 0) { 72 | return { 73 | text: '', 74 | extra: messageList 75 | } 76 | } else { 77 | return { 78 | text: '' 79 | } 80 | } 81 | } 82 | 83 | export const handle = (event, data, metadata, client, clientIndex, server) => { 84 | if (data.message.substr(0, 1) === '/') { 85 | server.handleCommand(data.message.substr(1), client, clientIndex) 86 | } else { 87 | const message = parseClassic(data.message) 88 | message.text = `<${client.username}> ` + message.text 89 | server.writeAll('chat', { 90 | message: JSON.stringify(message), 91 | position: 0 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Note** 2 | > 3 | > This experiment is archived. Take a look at https://github.com/vantezzen/electric-squid for an updated experiment of running a Minecraft server in the browser 4 | 5 |

6 |
7 |

8 | 9 | # Cauldron.js - Minecraft Server in your browser 10 | 11 | `Cauldron.js` allows you to run a minecraft server mainly from your browser. 12 | 13 | It still uses a NodeJS backend server that hosts the minecraft server but all work will be performed inside the browser. 14 | 15 | ## Why? 16 | By moving all processing and memory intensive work to the browser, `Cauldron.js` can be hosted on minimal server hardware while still allowing possibly hundreds of minecraft servers to be runnning on a single server. 17 | 18 | ## Why not? 19 | Tl:dr: Don't actually use CauldronJS to play Minecraft with your friends. 20 | 21 | 1. Moving all processes into the browser requires the whole server code to be rewritten. 22 | 23 | This is why `Cauldron.js` is still very bare-bones, having only a few elemental features and serving as a proof-of-concept. 24 | 25 | 2. Moving all processes into the browser also slows down all actions (a lot!). 26 | 27 | This results in actions sometimes needing up to a few seconds to get to the other clients, depending on the load. 28 | 29 | 3. `Cauldron.js` saves all game data (worlds etc.) inside IndexedDB. This limits the storage availible to 5MB on most mobile devices and up to 20GB on desktops with large hard drives, resulting in the size of the map being limited. 30 | 31 | ## Usage 32 | Try a demo of `Cauldron.js` on or [install it on your own server](#Installation). 33 | 34 | ## Installation 35 | 36 | 1. Clone this project 37 | ``` 38 | git clone https://github.com/vantezzen/cauldron-js.git 39 | ``` 40 | 2. Use `yarn` to install its dependecies 41 | ``` 42 | yarn install 43 | ``` 44 | 3. Start the server 45 | ``` 46 | yarn start 47 | ``` 48 | 4. Open the webpage in your browser (`127.0.0.1:3000` by default, the port will be printed in the console) 49 | 5. You will now see your minecraft server IP and port. Connect to the server in your minecraft client. 50 | 51 | ## Environment variables 52 | When starting `Cauldron.js` you can set the following environment variables: 53 | - DEBUG : Debug level for the NodeJS `debug` module (only for backend) 54 | - LOCATION : IP/domain the servers are availible at. If no supplied, `Cauldron.js` will use the NodeJS `ip` module to get the current server IP. 55 | 56 | ## Developing 57 | `Cauldron.js` uses Webpack to bundle its frontend. Please open a new terminal and run `yarn watch` to start webpack and listen for changes. 58 | 59 | `Cauldron.js` uses the following folder structure: 60 | ``` 61 | / 62 | client/ // Client/frontend files 63 | dist/ // Files generated by webpack 64 | js/ // Main server files 65 | index.js // Backend server 66 | ``` 67 | 68 | When developing `Cauldron.js` you might want to change its debug behaviour. `Cauldron.js` uses the npm `debug` module to debug both the front- and backend. To change the backend debug behaviour, set the `DEBUG` [environment variable](#Environment-variables), to change the frontend debug behaviour, set `localStorage.debug`. 69 | 70 | All of `Cauldron.js`'s modules will debug into the `cauldron:*` channel. 71 | 72 | 73 | Before creating a PR, please make sure to lint your code by running `yarn lint`. 74 | [![JavaScript Style Guide](https://cdn.rawgit.com/standard/standard/master/badge.svg)](https://github.com/standard/standard) 75 | 76 | ## Why no 1.14 support? 77 | `Cauldron.js` uses the `prismarine-chunk` library to create, save, dump and load chunks for the current world. Unfortunately, `prismarine-chunk` is not yet compatible with Minecraft 1.14. 78 | 79 | ## Contributing 80 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 81 | 82 | ## License 83 | [MIT](https://choosealicense.com/licenses/mit/) 84 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Cauldron.js 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Icon 19 |

20 | Cauldron.js 21 |

22 |

23 | Minecraft Server in your browser 24 |

25 |
26 |

Loading server files...

27 |
28 | 74 | 92 | 110 | 127 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * @version 0.1.0 5 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 6 | * @link https://github.com/vantezzen/cauldron-js 7 | * @license https://opensource.org/licenses/mit-license.php MIT License 8 | */ 9 | // Import libraries 10 | const chalk = require('chalk') 11 | const express = require('express') 12 | const http = require('http') 13 | const mc = require('minecraft-protocol') 14 | const path = require('path') 15 | const ip = require('ip') 16 | const debug = require('debug')('cauldron:backend') 17 | 18 | // Display welcome message 19 | console.log(chalk.cyan('Cauldron.js - Minecraft Server in your browser')) 20 | 21 | // Setup express server 22 | const app = express() 23 | app.use(express.static(path.join(__dirname, '/client'))) 24 | const server = http.createServer(app) 25 | // HTTP server routes 26 | app.get('/', function (req, res) { 27 | res.sendFile(path.join(__dirname, '/client/index.html')) 28 | }) 29 | 30 | // Setup socket.io server 31 | const io = require('socket.io')(server, { 32 | cookie: false 33 | }) 34 | 35 | const servers = {} // List of minecraft servers and clients connected 36 | let nextPort = 25500 // Port used for next server 37 | const availiblePorts = [] // Ports that are lower than `port` but are availible again 38 | 39 | const stopServer = (id, reason) => { 40 | // Disconnect all clients and show custom message 41 | Object.keys(servers[id].server.clients).forEach(clientId => { 42 | const client = servers[id].server.clients[clientId] 43 | client.end(reason) 44 | }) 45 | 46 | // Close server 47 | servers[id].server.close() 48 | 49 | // Make port availible for next server 50 | availiblePorts.push(servers[id].port) 51 | delete servers[id] 52 | debug('Server stopped, opening port for new server') 53 | } 54 | 55 | io.on('connection', socket => { 56 | debug('New socket connection') 57 | 58 | // Create new minecraft proxy server 59 | socket.on('create server', (version, motd, callback) => { 60 | // Only create one server per socket 61 | if (servers[socket.id]) callback(0) // eslint-disable-line 62 | 63 | // Get availible port for current server 64 | let port 65 | if (availiblePorts.length > 0) { 66 | port = availiblePorts.shift() 67 | } else { 68 | port = nextPort 69 | nextPort++ 70 | } 71 | 72 | // Setup minecraft proxy server 73 | const mcserver = mc.createServer({ 74 | 'online-mode': false, 75 | encryption: true, 76 | port, 77 | version, 78 | motd 79 | }) 80 | 81 | // Add server to servers list 82 | servers[socket.id] = { 83 | server: mcserver, 84 | socket, 85 | port, 86 | clients: {} 87 | } 88 | 89 | // Proxy all calls to the browser via socket.io 90 | mcserver.on('login', client => { 91 | servers[socket.id].clients[client.id] = client 92 | 93 | client.on('packet', (data, metadata) => { 94 | if (metadata.name) { 95 | socket.emit('event', metadata.name, data, metadata, client.id, client.uuid) 96 | } 97 | }) 98 | client.on('end', reason => { 99 | socket.emit('client disconnected', client.id, reason) 100 | }) 101 | socket.emit('login', client) 102 | }) 103 | debug('Started new MC proxy server on port ' + port) 104 | 105 | // Inform client about new server IP 106 | const location = process.env.LOCATION || ip.address() 107 | callback(`${location}:${port}`) // eslint-disable-line 108 | }) 109 | 110 | // Write a packet to a minecraft client 111 | socket.on('write', (client, type, data) => { 112 | if (servers[socket.id] && 113 | servers[socket.id].clients[client] && 114 | servers[socket.id].clients[client].write) { 115 | servers[socket.id].clients[client].write(type, data) 116 | } 117 | }) 118 | 119 | socket.on('motd', motd => { 120 | if (servers[socket.id] && servers[socket.id].server) { 121 | servers[socket.id].server.motd = motd 122 | } 123 | }) 124 | 125 | socket.on('stop', () => { 126 | if (servers[socket.id] && servers[socket.id].server) { 127 | stopServer(socket.id, 'You stopped the server. Please restart the server to continue playing.') 128 | } 129 | }) 130 | 131 | // Stop proxy server when socket disconnected 132 | socket.on('disconnect', () => { 133 | if (servers[socket.id] && servers[socket.id].server) { 134 | stopServer(socket.id, 'You closed your browser tab - this will stop your Minecraft server. Please reopen the tab to restart your server.') 135 | } 136 | }) 137 | }) 138 | 139 | // Start express server 140 | const expressport = process.env.PORT || 3000 141 | server.listen(expressport, function () { 142 | console.log(`Webserver listening on port ${expressport}`) 143 | }) 144 | -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * main.js - Entry point for frontend 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import io from 'socket.io-client' 12 | import Debugger from 'debug' 13 | import { 14 | openDatabase 15 | } from './storage' 16 | import MCServer from './mc-server' 17 | 18 | // Current MCServer instance 19 | let server 20 | 21 | // Set default debugging level 22 | if (!window.localStorage.debug) { 23 | window.localStorage.setItem('debug', 'cauldron:*') 24 | Debugger.enable('cauldron:*') 25 | } 26 | 27 | // Create debugger for server 28 | const debug = Debugger('cauldron:main') 29 | debug('Cauldron.JS - Minecraft Server in your browser.\nSource code availible at https://github.com/vantezzen/cauldron-js') 30 | 31 | // Open database 32 | const db = openDatabase() 33 | 34 | // Setup socket connection 35 | const socket = io() 36 | socket.on('connect', () => { 37 | debug('Connected socket to server with ID of', socket.io.engine.id) 38 | }) 39 | 40 | // Listen for new client login 41 | socket.on('login', client => { 42 | debug('New client connected with ID of', client.id) 43 | 44 | server.newClient(client) 45 | }) 46 | 47 | // Listen for minecraft client events 48 | socket.on('event', (event, data, metadata, id, uuid) => { 49 | server.handleEvent(event, data, metadata, id) 50 | }) 51 | 52 | // Listen for minecraft connection end 53 | socket.on('client disconnected', (client, reason) => { 54 | server.handleDisconnect(client, reason) 55 | }) 56 | 57 | // Helper function: Dump Dexie database to console 58 | window.dumpDB = async () => { 59 | db.tables.forEach(function (table, i) { 60 | var primKeyAndIndexes = [table.schema.primKey].concat(table.schema.indexes) 61 | var schemaSyntax = primKeyAndIndexes.map(function (index) { 62 | return index.src 63 | }).join(',') 64 | console.log('Table dump: ', table.name, schemaSyntax) 65 | table.each(function (object) { 66 | console.log(object) 67 | }) 68 | }) 69 | } 70 | 71 | // Start server using given settings 72 | const startServer = (version, motd, generator, seed) => { 73 | document.getElementById('start-server').style.display = 'none' 74 | document.getElementById('server-starting').style.display = 'block' 75 | 76 | // Tell backend to start new proxy server 77 | socket.emit('create server', version, motd, (ip) => { 78 | if (ip === 0) { 79 | debug('Error while starting server: Got response', ip) 80 | 81 | // Error while creating server 82 | document.getElementById('server-error').style.display = 'block' 83 | document.getElementById('server-starting').style.display = 'none' 84 | return 85 | } 86 | debug('Started MC Server proxy on IP', ip) 87 | 88 | // Get major version number 89 | if (version.split('.').length === 3) { 90 | version = version.split('.').slice(0, 2).join('.') 91 | } 92 | 93 | // Setup MC Server 94 | server = new MCServer(socket, version, db, generator, seed) 95 | 96 | // Show server online page with IP 97 | document.getElementById('server-online').style.display = 'block' 98 | document.getElementById('server-starting').style.display = 'none' 99 | document.getElementById('ip').innerText = ip 100 | }) 101 | } 102 | 103 | // Test if settings already saved => Start automatically 104 | document.getElementById('loading').style.display = 'none' 105 | if (window.localStorage.getItem('setting.version')) { 106 | const version = window.localStorage.getItem('setting.version') 107 | const motd = window.localStorage.getItem('setting.motd') 108 | const generator = window.localStorage.getItem('setting.generator') 109 | const seed = window.localStorage.getItem('setting.seed') 110 | 111 | debug('Auto-Starting MC Server with data:', version, motd, generator, seed) 112 | 113 | startServer(version, motd, generator, seed) 114 | } else { 115 | document.getElementById('start-server').style.display = 'block' 116 | } 117 | 118 | // Listen for click on start server button 119 | document.getElementById('start').addEventListener('click', () => { 120 | const version = document.getElementById('version').value 121 | const motd = document.getElementById('motd').value 122 | const generator = document.getElementById('generator').value 123 | const seed = document.getElementById('seed').value || String(Math.random()) 124 | 125 | // Save settings in window.localStorage 126 | window.localStorage.setItem('setting.version', version) 127 | window.localStorage.setItem('setting.motd', motd) 128 | window.localStorage.setItem('setting.generator', generator) 129 | window.localStorage.setItem('setting.seed', seed) 130 | 131 | debug('Starting MC Server with data:', version, motd, generator, seed) 132 | 133 | startServer(version, motd, generator, seed) 134 | }) 135 | 136 | // Listen for click on change settings/stop server button 137 | document.getElementById('stop').addEventListener('click', () => { 138 | server.stop() 139 | 140 | document.getElementById('version').value = window.localStorage.getItem('setting.version') 141 | document.getElementById('motd').value = window.localStorage.getItem('setting.motd') 142 | document.getElementById('generator').value = window.localStorage.getItem('setting.generator') 143 | document.getElementById('seed').value = window.localStorage.getItem('setting.seed') 144 | 145 | document.getElementById('start-server').style.display = 'block' 146 | document.getElementById('with-configured').style.display = 'block' 147 | document.getElementById('server-online').style.display = 'none' 148 | }) 149 | 150 | // Listen for server delete 151 | document.getElementById('delete').addEventListener('click', () => { 152 | document.getElementById('start-server').style.display = 'none' 153 | document.getElementById('server-delete').style.display = 'block' 154 | 155 | // Delete all data 156 | window.indexedDB.deleteDatabase('world') 157 | window.indexedDB.deleteDatabase('Minecraft') 158 | window.indexedDB.deleteDatabase('__dbnames') 159 | window.localStorage.clear() 160 | 161 | // Wait 2 seconds for deletion to complete 162 | setTimeout(() => { 163 | window.location.reload() 164 | }, 2000) 165 | }) 166 | -------------------------------------------------------------------------------- /client/css/style.css: -------------------------------------------------------------------------------- 1 | blockquote, 2 | li, 3 | ol, 4 | pre, 5 | ul { 6 | text-align: left 7 | } 8 | 9 | @media print { 10 | 11 | blockquote, 12 | img, 13 | pre, 14 | tr { 15 | page-break-inside: avoid 16 | } 17 | 18 | *, 19 | :after, 20 | :before { 21 | background: 0 0 !important; 22 | color: #000 !important; 23 | box-shadow: none !important; 24 | text-shadow: none !important 25 | } 26 | 27 | a, 28 | a:visited { 29 | text-decoration: underline 30 | } 31 | 32 | a[href]:after { 33 | content: " ("attr(href) ")" 34 | } 35 | 36 | abbr[title]:after { 37 | content: " ("attr(title) ")" 38 | } 39 | 40 | a[href^="#"]:after, 41 | a[href^="javascript:"]:after { 42 | content: "" 43 | } 44 | 45 | blockquote, 46 | pre { 47 | border: 1px solid #999 48 | } 49 | 50 | thead { 51 | display: table-header-group 52 | } 53 | 54 | img { 55 | max-width: 100% !important 56 | } 57 | 58 | h2, 59 | h3, 60 | p { 61 | orphans: 3; 62 | widows: 3 63 | } 64 | 65 | h2, 66 | h3 { 67 | page-break-after: avoid 68 | } 69 | } 70 | 71 | html { 72 | font-size: 12px 73 | } 74 | 75 | @media screen and (min-width:32rem) and (max-width:48rem) { 76 | html { 77 | font-size: 15px 78 | } 79 | } 80 | 81 | @media screen and (min-width:48rem) { 82 | html { 83 | font-size: 16px 84 | } 85 | } 86 | 87 | body { 88 | line-height: 1.85; 89 | margin: 1rem; 90 | } 91 | 92 | .air-p, 93 | p { 94 | font-size: 1rem; 95 | margin-bottom: 1.3rem 96 | } 97 | 98 | .air-h1, 99 | .air-h2, 100 | .air-h3, 101 | .air-h4, 102 | h1, 103 | h2, 104 | h3, 105 | h4 { 106 | margin: 1.414rem 0 .5rem; 107 | font-weight: inherit; 108 | line-height: 1.42 109 | } 110 | 111 | .air-h1, 112 | h1 { 113 | margin-top: 0; 114 | font-size: 3.998rem 115 | } 116 | 117 | .air-h2, 118 | h2 { 119 | font-size: 2.827rem 120 | } 121 | 122 | .air-h3, 123 | h3 { 124 | font-size: 1.999rem 125 | } 126 | 127 | .air-h4, 128 | h4 { 129 | font-size: 1.414rem 130 | } 131 | 132 | .air-h5, 133 | h5 { 134 | font-size: 1.121rem 135 | } 136 | 137 | .air-h6, 138 | h6 { 139 | font-size: .88rem 140 | } 141 | 142 | .air-small, 143 | small { 144 | font-size: .707em 145 | } 146 | 147 | canvas, 148 | iframe, 149 | img, 150 | select, 151 | svg, 152 | textarea, 153 | video { 154 | max-width: 100% 155 | } 156 | 157 | body { 158 | color: #444; 159 | font-family: 'Open Sans', Helvetica, sans-serif; 160 | font-weight: 300; 161 | margin: 3rem auto 1rem; 162 | max-width: 48rem; 163 | text-align: center 164 | } 165 | 166 | img { 167 | border-radius: 50%; 168 | height: 200px; 169 | margin: 0 auto; 170 | width: 200px 171 | } 172 | 173 | a, 174 | a:visited { 175 | color: #3498db 176 | } 177 | 178 | a:active, 179 | a:focus, 180 | a:hover { 181 | color: #2980b9 182 | } 183 | 184 | pre { 185 | background-color: #fafafa; 186 | padding: 1rem 187 | } 188 | 189 | blockquote { 190 | margin: 0; 191 | border-left: 5px solid #7a7a7a; 192 | font-style: italic; 193 | padding: 1.33em 194 | } 195 | 196 | p { 197 | color: #777 198 | } 199 | 200 | 201 | /* CUSTOM */ 202 | /* #start-server { 203 | 204 | } */ 205 | 206 | input { 207 | width: calc(100% - 2rem); 208 | padding: 1rem; 209 | outline: none; 210 | } 211 | 212 | select { 213 | background-color: #FFFFFF; 214 | 215 | width: 100%; 216 | 217 | outline: none; 218 | -webkit-appearance: none; 219 | -moz-appearance: none; 220 | appearance: none; 221 | display: inline-block; 222 | 223 | border-radius: 0px; 224 | overflow: hidden; 225 | 226 | font-size: 13px; 227 | line-height: 26px; 228 | padding: .5rem 1rem; 229 | margin: 0; 230 | } 231 | 232 | button { 233 | width: 70%; 234 | overflow: hidden; 235 | 236 | padding: 1rem; 237 | 238 | cursor: pointer; 239 | user-select: none; 240 | transition: all 150ms linear; 241 | text-align: center; 242 | white-space: nowrap; 243 | text-decoration: none !important; 244 | text-transform: none; 245 | 246 | color: #fff; 247 | border: 0 none; 248 | border-radius: 4px; 249 | 250 | font-size: 13px; 251 | font-weight: 500; 252 | line-height: 1.3; 253 | 254 | -webkit-appearance: none; 255 | -moz-appearance: none; 256 | appearance: none; 257 | 258 | justify-content: center; 259 | align-items: center; 260 | 261 | background-color: #416dea; 262 | 263 | box-shadow: 2px 5px 10px #416dea; 264 | } 265 | 266 | button#delete { 267 | background-color: #e33e24; 268 | 269 | box-shadow: 2px 5px 10px #e33e24; 270 | } 271 | 272 | img { 273 | height: 5rem; 274 | width: 5rem; 275 | } 276 | 277 | .mb { 278 | margin-bottom: 2rem; 279 | } 280 | 281 | .stats p { 282 | margin: 0; 283 | } 284 | 285 | #server-starting { 286 | margin-top: 4rem; 287 | } 288 | 289 | .loading-text { 290 | text-align: center; 291 | } 292 | 293 | .lds-grid { 294 | display: inline-block; 295 | position: relative; 296 | width: 64px; 297 | height: 64px; 298 | } 299 | 300 | .lds-grid div { 301 | position: absolute; 302 | width: 13px; 303 | height: 13px; 304 | border-radius: 50%; 305 | background: #212121; 306 | animation: lds-grid 1.2s linear infinite; 307 | } 308 | 309 | .lds-grid div:nth-child(1) { 310 | top: 6px; 311 | left: 6px; 312 | animation-delay: 0s; 313 | } 314 | 315 | .lds-grid div:nth-child(2) { 316 | top: 6px; 317 | left: 26px; 318 | animation-delay: -0.4s; 319 | } 320 | 321 | .lds-grid div:nth-child(3) { 322 | top: 6px; 323 | left: 45px; 324 | animation-delay: -0.8s; 325 | } 326 | 327 | .lds-grid div:nth-child(4) { 328 | top: 26px; 329 | left: 6px; 330 | animation-delay: -0.4s; 331 | } 332 | 333 | .lds-grid div:nth-child(5) { 334 | top: 26px; 335 | left: 26px; 336 | animation-delay: -0.8s; 337 | } 338 | 339 | .lds-grid div:nth-child(6) { 340 | top: 26px; 341 | left: 45px; 342 | animation-delay: -1.2s; 343 | } 344 | 345 | .lds-grid div:nth-child(7) { 346 | top: 45px; 347 | left: 6px; 348 | animation-delay: -0.8s; 349 | } 350 | 351 | .lds-grid div:nth-child(8) { 352 | top: 45px; 353 | left: 26px; 354 | animation-delay: -1.2s; 355 | } 356 | 357 | .lds-grid div:nth-child(9) { 358 | top: 45px; 359 | left: 45px; 360 | animation-delay: -1.6s; 361 | } 362 | 363 | @keyframes lds-grid { 364 | 365 | 0%, 366 | 100% { 367 | opacity: 1; 368 | } 369 | 370 | 50% { 371 | opacity: 0.5; 372 | } 373 | } -------------------------------------------------------------------------------- /client/js/generators/minecraft.js: -------------------------------------------------------------------------------- 1 | import Vec3 from 'vec3' 2 | import ChunkGen from 'prismarine-chunk' 3 | import NoiseGen from 'noisejs' 4 | 5 | // Return true with a given chance 6 | const withChance = chance => { 7 | return Math.random() < chance 8 | } 9 | 10 | /** 11 | * Generate a cluster of ore at a given coordinate 12 | * 13 | * @param {*} x X coordinate to start from 14 | * @param {*} y Y coordinate to start from 15 | * @param {*} z Z coordinate to start from 16 | * @param {*} clusterChance Chance for another block to spawn for clustering 17 | * @param {*} block Block ID to cluster 18 | * @param {*} chunk Chunk to cluster to 19 | */ 20 | const generateOreCluster = (x, y, z, clusterChance, block, chunk, dirtHeight) => { 21 | if (y < dirtHeight) { 22 | chunk.setBlockType(new Vec3(x, y, z), block) 23 | 24 | if (withChance(clusterChance) && x < 16) { 25 | generateOreCluster(x + 1, y, z, clusterChance, block, chunk, dirtHeight) 26 | } 27 | if (withChance(clusterChance) && x > 0) { 28 | generateOreCluster(x - 1, y, z, clusterChance, block, chunk, dirtHeight) 29 | } 30 | 31 | if (withChance(clusterChance) && y < dirtHeight - 3) { 32 | generateOreCluster(x, y + 1, z, clusterChance, block, chunk, dirtHeight) 33 | } 34 | if (withChance(clusterChance) && y > 1) { 35 | generateOreCluster(x, y - 1, z, clusterChance, block, chunk, dirtHeight) 36 | } 37 | 38 | if (withChance(clusterChance) && z < 16) { 39 | generateOreCluster(x, y, z + 1, clusterChance, block, chunk, dirtHeight) 40 | } 41 | if (withChance(clusterChance) && z > 0) { 42 | generateOreCluster(x, y, z - 1, clusterChance, block, chunk, dirtHeight) 43 | } 44 | } 45 | } 46 | 47 | const generateTree = (x, y, z, chunk) => { 48 | // Create trunk 49 | for (let yOff = 0; yOff < 4; yOff++) { 50 | chunk.setBlockType(new Vec3(x, y + yOff, z), 17) 51 | } 52 | 53 | // Create lower leaf layers 54 | for (let xOff = -2; xOff <= 2; xOff++) { 55 | for (let zOff = -2; zOff <= 2; zOff++) { 56 | for (let yOff = 2; yOff <= 3; yOff++) { 57 | if ( 58 | ((xOff !== 0 || zOff !== 0) && 59 | (xOff !== -2 || zOff !== -2) && 60 | (xOff !== 2 || zOff !== -2) && 61 | (xOff !== -2 || zOff !== 2) && 62 | (xOff !== 2 || zOff !== 2)) || withChance(1 / 4)) { 63 | chunk.setBlockType(new Vec3(x + xOff, y + yOff, z + zOff), 18) 64 | } 65 | } 66 | } 67 | } 68 | 69 | // Create upper leaf layers 70 | for (let xOff = -1; xOff <= 1; xOff++) { 71 | for (let zOff = -1; zOff <= 1; zOff++) { 72 | for (let yOff = 4; yOff <= 5; yOff++) { 73 | if (((xOff !== -1 || zOff !== -1) && 74 | (xOff !== 1 || zOff !== -1) && 75 | (xOff !== -1 || zOff !== 1) && 76 | (xOff !== 1 || zOff !== 1)) || (withChance(1 / 4) && yOff !== 5)) { 77 | chunk.setBlockType(new Vec3(x + xOff, y + yOff, z + zOff), 18) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | // Generate pond currently not in use as it needs to be reworked 85 | // const generatePond = (x, y, z, chunk) => { 86 | // let xMax = Math.floor(Math.random() * 3) + 3 87 | // let xMin = Math.floor(Math.random() * 3) + 3 88 | // let zMax = Math.floor(Math.random() * 3) + 3 89 | // let zMin = Math.floor(Math.random() * 3) + 3 90 | 91 | // if (x + xMax > 15) { 92 | // xMax = 15 - x 93 | // } 94 | // if (x - xMin < 0) { 95 | // xMin = x 96 | // } 97 | // if (z + zMax > 15) { 98 | // zMax = 15 - z 99 | // } 100 | // if (z - zMin < 0) { 101 | // zMin = z 102 | // } 103 | 104 | // const pondLength = (xMax + xMin) > (zMax + zMin) ? (xMax + xMin) : (zMax + zMin) 105 | 106 | // for (let xOff = -xMin; xOff <= xMax; xOff++) { 107 | // for (let zOff = -zMin; zOff <= zMax; zOff++) { 108 | // const depth = Math.floor(Math.abs((((xOff + zOff) / 2) / pondLength) - 0.5) * 4) 109 | 110 | // for (let yOff = 0; yOff <= depth; yOff++) { 111 | // chunk.setBlockType(new Vec3(x + xOff, y - yOff, z + zOff), 9) 112 | // } 113 | // } 114 | // } 115 | // } 116 | 117 | export default function generation ({ 118 | version, 119 | seed = 'Cauldron.JS', 120 | detalization = 50, 121 | minHeight = 50, 122 | maxHeight = 100 123 | } = {}) { 124 | const Chunk = ChunkGen(version) 125 | const Noise = new NoiseGen.Noise(seed) 126 | 127 | // Noise functions for alternative height generation methods 128 | // const elevation = new tumult.Perlin2(seed + 'elevation') 129 | // const roughness = new tumult.Perlin2(seed + 'roughness') 130 | // const detail = new tumult.Perlin2(seed + 'detail') 131 | 132 | function generateNoise (x, z) { 133 | const noise = (Noise.perlin2(x / detalization, z / detalization) + 1) * 0.5 134 | const range = maxHeight - minHeight 135 | 136 | return Math.round(noise * range + minHeight) 137 | } 138 | 139 | function generateChunk (chunkX, chunkZ) { 140 | const chunk = new Chunk() 141 | 142 | for (let x = 0; x < 16; x++) { 143 | for (let z = 0; z < 16; z++) { 144 | let height = generateNoise(chunkX * 16 + x, chunkZ * 16 + z) 145 | 146 | // Alternative height generation methods 147 | // let height = Math.round(Math.abs(elevation.gen((chunkX * 16 + x) / 100, (chunkZ * 16 + z) / 100) + (roughness.gen((chunkX * 16 + x) / 100, (chunkZ * 16 + z) / 100)) * detail.gen((chunkX * 16 + x) / 100, (chunkZ * 16 + z) / 100) * 16 + 64)); 148 | // let height = Math.round(Math.abs(noise.gen((chunkX * 16 + x) / 100, (chunkZ * 16 + z) / 100))) + 20; 149 | 150 | // Keep height between 200 and 10 151 | if (height > 200) { 152 | height = 200 153 | } else if (height < 10 || isNaN(height)) { 154 | height = 10 155 | } 156 | 157 | // Dirt layer should be between 2 and 8 layer thick 158 | const dirtHeight = height - (Math.floor(Math.random() * 6) + 2) 159 | // Grass layer is at height limit 160 | const grassHeight = height 161 | 162 | // Generate bedrock layer 163 | chunk.setBlockType(new Vec3(x, 0, z), 7) 164 | // Generate stone layer 165 | for (let y = 1; y < dirtHeight; y++) { 166 | if (chunk.getBlockType(new Vec3(x, y, z)) === 0) { 167 | chunk.setBlockType(new Vec3(x, y, z), 1) 168 | 169 | // Generate random iron ore clusters 170 | if (withChance(1 / 512)) { 171 | generateOreCluster(x, y, z, 1 / 8, 15, chunk, dirtHeight) 172 | } 173 | // Generate random diamond ore clusters 174 | if (y < 20 && withChance(1 / 2048)) { 175 | generateOreCluster(x, y, z, 1 / 16, 56, chunk, dirtHeight) 176 | } 177 | } 178 | } 179 | 180 | // Generate dirt layer 181 | for (let y = dirtHeight; y < grassHeight; y++) { 182 | if (chunk.getBlockType(new Vec3(x, y, z)) === 0) { 183 | chunk.setBlockType(new Vec3(x, y, z), 3) 184 | } 185 | } 186 | // Generate grass layer 187 | if (chunk.getBlockType(new Vec3(x, grassHeight, z)) === 0) { 188 | chunk.setBlockType(new Vec3(x, grassHeight, z), 2) 189 | } 190 | 191 | // Generate trees 192 | if (z < 13 && z > 4 && x < 13 && x > 4 && withChance(1 / 32)) { 193 | generateTree(x, grassHeight + 1, z, chunk) 194 | } 195 | // Generate ponds 196 | if (z < 13 && z > 4 && x < 13 && x > 4 && withChance(1 / 128)) { 197 | // generatePond(x, grassHeight, z, chunk); 198 | } 199 | 200 | for (let y = 0; y < 256; y++) { 201 | chunk.setSkyLight(new Vec3(x, y, z), 15) 202 | } 203 | } 204 | } 205 | return chunk 206 | } 207 | return generateChunk 208 | } 209 | -------------------------------------------------------------------------------- /client/js/world.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * MCWorld - Manage minecraft world and its chunks 5 | * 6 | * Based on nodecraft's land system (https://github.com/YaroslavGaponov/nodecraft/blob/628a256935/src/land/index.js) 7 | * and flying-squid's world system (https://github.com/PrismarineJS/flying-squid/blob/master/src/lib/plugins/world.js) 8 | * 9 | * @version 0.1.0 10 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 11 | * @link https://github.com/vantezzen/cauldron-js 12 | * @license https://opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | import events from 'events' 15 | import ChunkLoader from 'prismarine-chunk' 16 | import Vec3 from 'vec3' 17 | import generators from './generators' 18 | import localForage from 'localforage' 19 | import Spiralloop from 'spiralloop' 20 | 21 | import Debugger from 'debug' 22 | const debug = Debugger('cauldron:mc-world') 23 | 24 | const EventEmitter = events.EventEmitter 25 | 26 | // Format bytes to human-readable size (Source: https://stackoverflow.com/a/18650828/10590162) 27 | function formatBytes (bytes, decimals = 2) { 28 | if (bytes === 0) return '0 Bytes' 29 | 30 | const k = 1024 31 | const dm = decimals < 0 ? 0 : decimals 32 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 33 | 34 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 35 | 36 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] 37 | } 38 | 39 | export default class MCWorld extends EventEmitter { 40 | constructor (version, generator, server) { 41 | super() 42 | 43 | this.server = server 44 | 45 | this._map = new Map() // Saving a map of current chunks 46 | this.version = version // Current minecraft version 47 | this.generator = generators[generator]({ 48 | version, 49 | seed: server.seed 50 | }) // Generator to use 51 | this.Chunk = ChunkLoader(version) // Chunk version to use 52 | 53 | // Initialise localForage for storing minecraft world 54 | localForage.config({ 55 | name: 'world', 56 | version: 1.0, 57 | storeName: 'world_land', 58 | description: 'Cauldron.JS Minecraft World' 59 | }) 60 | 61 | // Save world every 10 seconds 62 | this.save = this.save.bind(this) 63 | setInterval(this.save, 10 * 1000) 64 | 65 | this.updateWorldStats() 66 | 67 | debug('Constructed new MCWorld') 68 | } 69 | 70 | // Update stats about world on page 71 | updateWorldStats () { 72 | document.getElementById('chunks').innerText = this._map.size 73 | if (navigator.storage && navigator.storage.estimate) { 74 | navigator.storage.estimate().then(estimate => { 75 | const usage = Math.round((estimate.usage / estimate.quota) * 100) 76 | 77 | const text = `${usage}% (${formatBytes(estimate.usage, 0)} of ${formatBytes(estimate.quota, 0)})` 78 | 79 | document.getElementById('storage').innerText = text 80 | }) 81 | } 82 | } 83 | 84 | // Clean map of loaded chunks to only contain up to 50 chunks 85 | cleanLoadedChunks () { 86 | const maxChunks = 50 87 | 88 | // Keep number of loaded chunks below maxChunks 89 | const entries = this._map.entries() 90 | while (this._map.size > maxChunks) { 91 | this._map.delete(entries.next().value[0]) 92 | } 93 | } 94 | 95 | // Save world to localForage 96 | save () { 97 | debug('Saving ' + this._map.size + ' chunks') 98 | 99 | this._map.forEach((chunk, id) => { 100 | const dump = chunk.dump() 101 | 102 | // Convert dump to string 103 | let data = '' 104 | dump.forEach(function (byte) { 105 | data += String.fromCharCode(byte) 106 | }) 107 | 108 | localForage.setItem('r-' + id, data) 109 | }) 110 | this.cleanLoadedChunks() 111 | this.updateWorldStats() 112 | } 113 | 114 | forEachChunk (fn) { 115 | this._initializer = fn 116 | } 117 | 118 | // Send chunks that are near a player to the client 119 | sendNearbyChunks (x, z, id) { 120 | const chunkX = x >> 4 121 | const chunkZ = z >> 4 122 | // Chunk radius to send to client 123 | const distance = 5 124 | 125 | // Send nearby chunks in spiralloop to send nearest chunks first 126 | Spiralloop([distance * 2, distance * 2], (xPos, zPos) => { 127 | const x = chunkX - distance + xPos 128 | const z = chunkZ - distance + zPos 129 | 130 | const chunkId = `${x}:${z}` 131 | if (!this.server.clientChunks.get(id).has(chunkId)) { 132 | this.server.clientChunks.get(id).add(chunkId) 133 | this.getChunk(x, z).then(chunk => { 134 | this.sendChunk(id, x, z, chunk.dump()) 135 | }) 136 | } 137 | }) 138 | 139 | this.cleanLoadedChunks() 140 | } 141 | 142 | // Get prismarine-chunk object for a chunk 143 | async getChunk (chunkX, chunkZ) { 144 | return new Promise(async resolve => { // eslint-disable-line 145 | const chunkID = `${chunkX}:${chunkZ}` 146 | if (!this._map.has(chunkID)) { 147 | let chunk 148 | const data = await localForage.getItem('r-' + chunkID) 149 | if (data) { 150 | // Load data from localStorage 151 | chunk = new this.Chunk() 152 | 153 | const dump = new Uint8Array(data.length) 154 | for (let i = 0, strLen = data.length; i < strLen; i++) { 155 | dump[i] = data.charCodeAt(i) 156 | } 157 | 158 | chunk.load(Buffer.from(dump)) 159 | 160 | // Fill chunk with light 161 | for (let x = 0; x < 16; x++) { 162 | for (let z = 0; z < 16; z++) { 163 | for (let y = 0; y < 256; y++) { 164 | chunk.setSkyLight(new Vec3(x, y, z), 15) 165 | } 166 | } 167 | } 168 | } else { 169 | // Generate new chunk 170 | chunk = this.generator(chunkX, chunkZ) 171 | } 172 | this._map.set(chunkID, chunk) 173 | } 174 | resolve(this._map.get(chunkID)) 175 | this.updateWorldStats() 176 | }) 177 | } 178 | 179 | async setBlock (x, y, z, block) { 180 | const chunkX = x >> 4 181 | const chunkZ = z >> 4 182 | const chunk = await this.getChunk(chunkX, chunkZ) 183 | chunk.setBlock(new Vec3(x & 0x0f, y, z & 0x0f), block) 184 | this.emit('changed', chunkX, chunkZ) 185 | }; 186 | 187 | getBlock (x, y, z) { 188 | return new Promise(async resolve => { // eslint-disable-line 189 | const chunkX = x >> 4 190 | const chunkZ = z >> 4 191 | const chunk = await this.getChunk(chunkX, chunkZ) 192 | const block = chunk.getBlock(new Vec3(x & 0x0f, y, z & 0x0f)) 193 | resolve(block) 194 | }) 195 | }; 196 | 197 | async setType (x, y, z, type) { 198 | const chunkX = x >> 4 199 | const chunkZ = z >> 4 200 | const chunk = await this.getChunk(chunkX, chunkZ) 201 | chunk.setBlockType(new Vec3(x & 0x0f, y, z & 0x0f), type) 202 | this.emit('changed', chunkX, chunkZ) 203 | } 204 | 205 | getType (x, y, z) { 206 | return new Promise(async resolve => { // eslint-disable-line 207 | const chunkX = x >> 4 208 | const chunkZ = z >> 4 209 | const chunk = await this.getChunk(chunkX, chunkZ) 210 | resolve(chunk.getBlockType(new Vec3(x & 0x0f, y, z & 0x0f))) 211 | }) 212 | } 213 | 214 | async setLightBlock (x, y, z, light) { 215 | const chunkX = x >> 4 216 | const chunkZ = z >> 4 217 | const chunk = await this.getChunk(chunkX, chunkZ) 218 | chunk.setBlockLight(new Vec3(x & 0x0f, y, z & 0x0f), light) 219 | this.emit('changed', chunkX, chunkZ) 220 | return this 221 | } 222 | 223 | getLightBlock (x, y, z) { 224 | return new Promise(async resolve => { // eslint-disable-line 225 | const chunkX = x >> 4 226 | const chunkZ = z >> 4 227 | const chunk = await this.getChunk(chunkX, chunkZ) 228 | resolve(chunk.getBlockLight(new Vec3(x & 0x0f, y, z & 0x0f))) 229 | }) 230 | } 231 | 232 | async setLightSky (x, y, z, light) { 233 | const chunkX = x >> 4 234 | const chunkZ = z >> 4 235 | const chunk = await this.getChunk(chunkX, chunkZ) 236 | chunk.setSkyLight(new Vec3(x & 0x0f, y, z & 0x0f), light) 237 | this.emit('changed', chunkX, chunkZ) 238 | return this 239 | } 240 | 241 | getLightSky (x, y, z) { 242 | return new Promise(async resolve => { // eslint-disable-line 243 | const chunkX = x >> 4 244 | const chunkZ = z >> 4 245 | const chunk = await this.getChunk(chunkX, chunkZ) 246 | resolve(chunk.getSkyLight(new Vec3(x & 0x0f, y, z & 0x0f))) 247 | }) 248 | } 249 | 250 | async setBiome (x, z, biome) { 251 | const chunkX = x >> 4 252 | const chunkZ = z >> 4 253 | const chunk = await this.getChunk(chunkX, chunkZ) 254 | chunk.setBiome(new Vec3(x & 0x0f, 0, z & 0x0f), biome) 255 | this.emit('changed', chunkX, chunkZ) 256 | return this 257 | } 258 | 259 | getBiome (x, z) { 260 | return new Promise(async resolve => { // eslint-disable-line 261 | const chunkX = x >> 4 262 | const chunkZ = z >> 4 263 | const chunk = await this.getChunk(chunkX, chunkZ) 264 | resolve(chunk.getBiome(new Vec3(x & 0x0f, 0, z & 0x0f))) 265 | }) 266 | } 267 | 268 | sendChunk (playerId, x, z, chunk) { 269 | this.server.write(playerId, 'map_chunk', { 270 | x: x, 271 | z: z, 272 | groundUp: true, 273 | bitMap: 0xffff, 274 | primary_bitmap: 65535, 275 | add_bitmap: 65535, 276 | chunkData: chunk, 277 | blockEntities: [] 278 | }) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /client/js/mc-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cauldron.js - Minecraft Server in your browser 3 | * 4 | * mc-server.js - MCServer - Manage server 5 | * 6 | * @version 0.1.0 7 | * @copyright Copyright vantezzen (https://github.com/vantezzen) 8 | * @link https://github.com/vantezzen/cauldron-js 9 | * @license https://opensource.org/licenses/mit-license.php MIT License 10 | */ 11 | import Debugger from 'debug' 12 | import Vec3 from 'vec3' 13 | import MCEvent from './event' 14 | import MCWorld from './world' 15 | import MCCommand from './command' 16 | 17 | // Create debugger for MCServer 18 | const debug = Debugger('cauldron:mc-server') 19 | 20 | export default class MCServer { 21 | constructor (socket, version, db, generator, seed) { 22 | this.socket = socket // Current socket connection to backend 23 | this.db = db // Dexie database instance 24 | this.seed = seed 25 | this.version = version 26 | this.generator = generator 27 | this.event = new MCEvent() // Event handler for minecraft client events 28 | this.world = new MCWorld(version, generator, this) // Overworld 29 | this.command = new MCCommand(this) // Handler for minecraft commands 30 | 31 | // Array of minecraft clients currently connected 32 | this.clients = [] 33 | 34 | // Keep map of what chunks a client has loaded 35 | this.clientChunks = new Map() 36 | 37 | // Keep client settings 38 | this.clientSettings = {} 39 | 40 | // Load all plugins 41 | this.loadPlugins() 42 | 43 | // Show current stats 44 | this.updateServerStats() 45 | 46 | // Start interval to check performance 47 | this.checkPerformance() 48 | 49 | debug('Started MC Server') 50 | } 51 | 52 | // Include all plugins inside the plugins/ directory 53 | loadPlugins () { 54 | const plugins = require.context('./plugins', true, /.*\.js$/) 55 | plugins.keys().forEach((plugin) => { 56 | const init = plugins(plugin).default 57 | init(this) 58 | }) 59 | } 60 | 61 | // Convert float (degrees) --> byte (1/256 "degrees"), needed for head rotation 62 | // Source: https://github.com/PrismarineJS/flying-squid/blob/43b665bb84afc44f58758671d5a1e8bc75809cbe/src/lib/plugins/updatePositions.js 63 | conv (f) { 64 | let b = Math.floor((f % 360) * 256 / 360) 65 | if (b < -128) b += 256 66 | else if (b > 127) b -= 256 67 | return b 68 | } 69 | 70 | // Update stats about MC Server on page 71 | updateServerStats () { 72 | document.getElementById('players').innerText = this.clients.length 73 | } 74 | 75 | // Check JavaScript performance to guesstimate CPU load 76 | // Check time it takes for the browser between executing a 0ms interval 77 | // This should give us a very rough estimation but is only really useful 78 | // to check if the CPU load is very high. 79 | // This results in the CPU load percentage shown not really being too meaningful 80 | checkPerformance () { 81 | let last = new Date().getTime() 82 | let intervalsSince = 0 83 | 84 | setInterval(() => { 85 | if (intervalsSince > 50) { 86 | intervalsSince = 0 87 | const now = new Date().getTime() 88 | let load = Math.round(((now - last) / 50) * 10) 89 | last = new Date().getTime() 90 | 91 | if (load > 100) { 92 | load = '>100' 93 | } 94 | 95 | document.getElementById('cpu').innerText = load + '%' 96 | } 97 | 98 | intervalsSince++ 99 | }, 0) 100 | } 101 | 102 | // Handle new client connecting to server 103 | newClient (client) { 104 | // Add additional info to client 105 | client.gameMode = 1 106 | 107 | // Add client to clients list 108 | this.clients.push(client) 109 | const clientIndex = this.clients.findIndex(el => el.id === client.id) 110 | this.clientChunks.set(client.id, new Set()) 111 | this.updateServerStats() 112 | debug('New client connected with ID of', client.id) 113 | 114 | // Write login package 115 | this.write(client.id, 'login', { 116 | entityId: client.id, 117 | levelType: 'default', 118 | gameMode: 1, 119 | dimension: 0, 120 | difficulty: 2, 121 | maxPlayers: 10, 122 | reducedDebugInfo: false 123 | }) 124 | debug('Written login package to new client') 125 | 126 | // Update tab lists for all players 127 | this.writeAll('player_info', { 128 | action: 0, 129 | data: this.clients.map((otherPlayer) => ({ 130 | UUID: otherPlayer.uuid, 131 | name: otherPlayer.username, 132 | properties: [] 133 | })) 134 | }) 135 | 136 | // Get player position from database 137 | this.db.players.get(client.uuid) 138 | .then(data => { 139 | if (!data) { 140 | // Player not yet in db, add new 141 | debug('Adding new player to database') 142 | 143 | this.db.players.add({ 144 | uuid: client.uuid, 145 | x: 15, 146 | y: 101, 147 | z: 15, 148 | yaw: 137, 149 | pitch: 0 150 | }) 151 | 152 | data = {} 153 | } 154 | const pos = { 155 | x: data.x || 15, 156 | y: data.y || 101, 157 | z: data.z || 15 158 | } 159 | 160 | debug('Writing position package to new client') 161 | this.write(client.id, 'position', { 162 | ...pos, 163 | yaw: data.yaw || 137, 164 | pitch: data.pitch || 0, 165 | flags: 0x00 166 | }) 167 | 168 | this.clients[clientIndex].position = { 169 | ...pos, 170 | yaw: data.yaw || 137, 171 | pitch: data.pitch || 0, 172 | onGround: data.onGround || false 173 | } 174 | 175 | // Send nearby chunks to client 176 | this.world.sendNearbyChunks(data.x || 15, data.z || 15, client.id) 177 | 178 | // Teleport player to new position 179 | const entityPosition = new Vec3( 180 | pos.x, 181 | pos.y, 182 | pos.z 183 | ).scaled(32).floored() 184 | 185 | this.writeOthers(client.id, 'named_entity_spawn', { 186 | entityId: client.id, 187 | playerUUID: client.uuid, 188 | x: entityPosition.x, 189 | y: entityPosition.y, 190 | z: entityPosition.z, 191 | yaw: this.conv(data.yaw) || 0, 192 | pitch: this.conv(data.pitch) || 0, 193 | currentItem: 0, 194 | metadata: [] 195 | }) 196 | this.writeOthers(client.id, 'entity_teleport', { 197 | entityId: client.id, 198 | onGround: data.onGround, 199 | ...pos 200 | }) 201 | }) 202 | 203 | // Show welcome message to all users 204 | debug('Writing welcome message package for new client') 205 | const msg = { 206 | translate: 'chat.type.announcement', 207 | with: [ 208 | 'CauldronJS', 209 | client.username + ' joined the server.' 210 | ] 211 | } 212 | this.writeAll('chat', { 213 | message: JSON.stringify(msg), 214 | position: 0 215 | }) 216 | 217 | // Spawn other players 218 | for (const player of this.clients) { 219 | if (player.id !== client.id) { 220 | const entityPosition = new Vec3( 221 | player.position.x || 15, 222 | player.position.y || 101, 223 | player.position.z || 15 224 | ).scaled(32).floored() 225 | 226 | this.write(client.id, 'named_entity_spawn', { 227 | entityId: player.id, 228 | playerUUID: player.uuid, 229 | x: entityPosition.x, 230 | y: entityPosition.y, 231 | z: entityPosition.z, 232 | yaw: this.conv(player.position.yaw) || 0, 233 | pitch: this.conv(player.position.pitch) || 0, 234 | currentItem: 0, 235 | metadata: [] 236 | }) 237 | this.write(client.id, 'entity_teleport', { 238 | entityId: player.id, 239 | x: player.position.x || 15, 240 | y: player.position.y || 101, 241 | z: player.position.z || 15, 242 | onGround: player.position.onGround 243 | }) 244 | } 245 | } 246 | debug('Spawned other players for new client') 247 | 248 | this.event.handle('login', {}, {}, client, clientIndex, this) 249 | } 250 | 251 | // Handle client disconnecting from server 252 | handleDisconnect (client, reason) { 253 | this.clients = this.clients.filter(el => el.id !== client) 254 | this.clientChunks.delete(client) 255 | 256 | this.updateServerStats() 257 | 258 | debug('Client disconnected from server') 259 | } 260 | 261 | // Handle event from minecraft client 262 | handleEvent (event, data, metadata, id) { 263 | const client = this.clients.findIndex(el => el.id === id) 264 | this.event.handle(event, data, metadata, this.clients[client], client, this) 265 | } 266 | 267 | // Handle command from player 268 | handleCommand (command, client, clientIndex) { 269 | this.command.handle(command, client, clientIndex) 270 | } 271 | 272 | // Send message to client 273 | sendMessage (id, text) { 274 | const msg = { 275 | translate: 'chat.type.announcement', 276 | with: [ 277 | 'CauldronJS', 278 | text 279 | ] 280 | } 281 | this.write(id, 'chat', { 282 | message: JSON.stringify(msg), 283 | position: 0 284 | }) 285 | } 286 | 287 | // Write a package to all players 288 | writeAll (type, data) { 289 | for (const player of this.clients) { 290 | this.write(player.id, type, data) 291 | } 292 | } 293 | 294 | // Write a package to all player except one 295 | writeOthers (clientId, type, data) { 296 | for (const player of this.clients) { 297 | if (player.id !== clientId) { 298 | this.write(player.id, type, data) 299 | } 300 | } 301 | } 302 | 303 | // Write a package to one player 304 | write (clientId, type, data) { 305 | this.socket.emit('write', clientId, type, data) 306 | } 307 | 308 | // Stop the server 309 | stop () { 310 | this.socket.emit('stop') 311 | } 312 | } 313 | --------------------------------------------------------------------------------