├── .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 | [](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 |
19 |
20 | Cauldron.js
21 |
22 |
23 | Minecraft Server in your browser
24 |
25 |
26 |
Loading server files...
27 |
28 |
29 |
Please select your settings and start your server.
30 |
31 |
MOTD
32 |
33 |
34 |
Version
35 |
36 | 1.8
37 | 1.9
38 | 1.10
39 | 1.11
40 | 1.11.2
41 | 1.12
42 | 1.12.2
43 | 1.13
44 | 1.13.2
45 | 1.14 (not yet supported)
46 |
47 |
48 |
World generator
49 |
50 | minecraft
51 | grass
52 | nether
53 | all_the_blocks
54 | superflat
55 |
56 |
57 |
Seed
58 |
59 |
60 |
61 | Start server
62 |
63 |
64 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
Starting server...
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
Deleting server files...
107 |
108 |
109 |
110 |
111 |
Your server is availible at
112 |
113 |
114 |
115 | Change settings
116 |
117 |
Changing settings will stop your server
118 |
119 |
120 |
Stats:
121 |
Players connected: ?
122 |
Loaded chunks: ?
123 |
CPU usage: ?
124 |
Storage usage: ?
125 |
126 |
127 |
128 |
We could not start your server. Please only open one server per socket connection.
129 |
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 |
--------------------------------------------------------------------------------