diff --git a/.gitignore b/.gitignore index 3ff87f1..a0031ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ package-lock.json -.vscode \ No newline at end of file +.vscode +public \ No newline at end of file diff --git a/README.md b/README.md index 320d7c4..c334a34 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,33 @@ -# prismarine-template -[![NPM version](https://img.shields.io/npm/v/prismarine-template.svg)](http://npmjs.com/package/prismarine-template) -[![Build Status](https://github.com/PrismarineJS/prismarine-template/workflows/CI/badge.svg)](https://github.com/PrismarineJS/prismarine-template/actions?query=workflow%3A%22CI%22) +# prismarine-web-client +[![NPM version](https://img.shields.io/npm/v/prismarine-web-client.svg)](http://npmjs.com/package/prismarine-web-client) +[![Build Status](https://github.com/PrismarineJS/prismarine-web-client/workflows/CI/badge.svg)](https://github.com/PrismarineJS/prismarine-web-client/actions?query=workflow%3A%22CI%22) [![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/GsEFRM8) -[![Gitter](https://img.shields.io/badge/chat-on%20gitter-brightgreen.svg)](https://gitter.im/PrismarineJS/general) -[![Irc](https://img.shields.io/badge/chat-on%20irc-brightgreen.svg)](https://irc.gitter.im/) -[![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-template) +[![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client) -A template repository to make it easy to create new prismarine repo +A minecraft client running in a web page. -## Usage +It runs mineflayer in the browser which connects to a websocket minecraft server. +It provides a simple websocket to tcp proxy as a backend to make it possible to connect to any minecraft server. + +## Features + +* display blocks +* display entities as colored rectangles +* movement sync + +## Roadmap + +* chat +* block placing and breaking + +## Run ```js -const template = require('prismarine-template') - -template.helloWorld() +npm install +npm run build-start ``` -## API +Then connect to http://localhost:8080 + -### helloWorld() -Prints hello world diff --git a/dns.js b/dns.js new file mode 100644 index 0000000..be7206a --- /dev/null +++ b/dns.js @@ -0,0 +1,39 @@ +/* global XMLHttpRequest */ + +// Custom DNS resolver made by SiebeDW. Powered by google dns. +// Supported: SRV (not all errors support) +module.exports.resolveSrv = function (hostname, callback) { + const Http = new XMLHttpRequest() + const url = `https://dns.google.com/resolve?name=${hostname}&type=SRV` + Http.open('GET', url) + Http.responseType = 'json' + Http.send() + + Http.onload = function () { + const response = Http.response + if (response.Status === 3) { + const err = new Error('querySrv ENOTFOUND') + err.code = 'ENOTFOUND' + callback(err) + return + } + if (!response.Answer || response.Answer.length < 1) { + const err = new Error('querySrv ENODATA') + err.code = 'ENODATA' + callback(err) + return + } + const willreturn = [] + response.Answer.forEach(function (object) { + const data = object.data.split(' ') + willreturn.push({ + priority: data[0], + weight: data[1], + port: data[2], + name: data[3] + }) + }) + console.log(willreturn) + callback(null, willreturn) + } +} diff --git a/example.js b/example.js deleted file mode 100644 index ceadd46..0000000 --- a/example.js +++ /dev/null @@ -1,3 +0,0 @@ -const template = require('prismarine-template') - -template.helloWorld() diff --git a/index.html b/index.html new file mode 100644 index 0000000..beddd64 --- /dev/null +++ b/index.html @@ -0,0 +1,30 @@ + + + + Prismarine Viewer + + + + + + diff --git a/index.js b/index.js index 4bd4c75..23ee193 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,132 @@ -if (typeof process !== 'undefined' && parseInt(process.versions.node.split('.')[0]) < 14) { - console.error('Your node version is currently', process.versions.node) - console.error('Please update it to a version >= 14.x.x from https://nodejs.org/') - process.exit(1) -} +/* global THREE, prompt */ -module.exports.helloWorld = function () { - console.log('Hello world !') +// Workaround for process.versions.node not existing in the browser +process.versions.node = '14.0.0' + +const mineflayer = require('mineflayer') +const { WorldView, Viewer } = require('prismarine-viewer/viewer') +global.THREE = require('three') + +async function main () { + const viewDistance = 6 + const host = prompt('Host', '95.111.249.143') + const port = parseInt(prompt('Port', '10000')) + const username = prompt('Username', 'pviewer_person') + let password = prompt('Password (blank for offline)') + password = password === '' ? undefined : password + console.log(`connecting to ${host} ${port} with ${username}`) + + const bot = mineflayer.createBot({ + host, + port, + username, + password + }) + + bot.on('end', () => { + console.log('disconnected') + }) + + bot.once('spawn', () => { + console.log('bot spawned - starting viewer') + + const version = bot.version + + const center = bot.entity.position + + const worldView = new WorldView(bot.world, viewDistance, center) + + // Create three.js context, add to page + const renderer = new THREE.WebGLRenderer() + renderer.setPixelRatio(window.devicePixelRatio || 1) + renderer.setSize(window.innerWidth, window.innerHeight) + document.body.appendChild(renderer.domElement) + + // Create viewer + const viewer = new Viewer(renderer) + viewer.setVersion(version) + + worldView.listenToBot(bot) + worldView.init(bot.entity.position) + + function botPosition () { + viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + worldView.updatePosition(bot.entity.position) + } + + bot.on('move', botPosition) + + // Link WorldView and Viewer + viewer.listen(worldView) + viewer.camera.position.set(center.x, center.y, center.z) + + function moveCallback (e) { + bot.entity.pitch -= e.movementY * 0.01 + bot.entity.yaw -= e.movementX * 0.01 + viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch) + } + function changeCallback () { + if (document.pointerLockElement === renderer.domElement || + document.mozPointerLockElement === renderer.domElement || + document.webkitPointerLockElement === renderer.domElement) { + document.addEventListener('mousemove', moveCallback, false) + } else { + document.removeEventListener('mousemove', moveCallback, false) + } + } + document.addEventListener('pointerlockchange', changeCallback, false) + document.addEventListener('mozpointerlockchange', changeCallback, false) + document.addEventListener('webkitpointerlockchange', changeCallback, false) + renderer.domElement.requestPointerLock = renderer.domElement.requestPointerLock || + renderer.domElement.mozRequestPointerLock || + renderer.domElement.webkitRequestPointerLock + document.addEventListener('mousedown', (e) => { + renderer.domElement.requestPointerLock() + }) + + document.addEventListener('contextmenu', (e) => e.preventDefault(), false) + document.addEventListener('keydown', (e) => { + console.log(e.code) + if (e.code === 'KeyW') { + bot.setControlState('forward', true) + } else if (e.code === 'KeyS') { + bot.setControlState('back', true) + } else if (e.code === 'KeyA') { + bot.setControlState('right', true) + } else if (e.code === 'KeyD') { + bot.setControlState('left', true) + } else if (e.code === 'Space') { + bot.setControlState('jump', true) + } else if (e.code === 'ShiftLeft') { + bot.setControlState('sneak', true) + } else if (e.code === 'ControlLeft') { + bot.setControlState('sprint', true) + } + }, false) + document.addEventListener('keyup', (e) => { + if (e.code === 'KeyW') { + bot.setControlState('forward', false) + } else if (e.code === 'KeyS') { + bot.setControlState('back', false) + } else if (e.code === 'KeyA') { + bot.setControlState('right', false) + } else if (e.code === 'KeyD') { + bot.setControlState('left', false) + } else if (e.code === 'Space') { + bot.setControlState('jump', false) + } else if (e.code === 'ShiftLeft') { + bot.setControlState('sneak', false) + } else if (e.code === 'ControlLeft') { + bot.setControlState('sprint', false) + } + }, false) + + // Browser animation loop + const animate = () => { + window.requestAnimationFrame(animate) + renderer.render(viewer.scene, viewer.camera) + } + animate() + }) } +main() diff --git a/package.json b/package.json index 3f1a05d..50f0e3a 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,50 @@ { - "name": "prismarine-template", - "version": "1.0.0", - "description": "A template repository to make it easy to create new prismarine repo", - "main": "index.js", - "scripts": { - "test": "jest --verbose", - "pretest": "npm run lint", - "lint": "standard", - "fix": "standard --fix" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/PrismarineJS/prismarine-template.git" - }, - "keywords": [ - "prismarine", - "template" - ], - "author": "Romain Beaumont", - "license": "MIT", - "bugs": { - "url": "https://github.com/PrismarineJS/prismarine-template/issues" - }, - "homepage": "https://github.com/PrismarineJS/prismarine-template#readme", - "devDependencies": { - "jest": "^26.1.0", - "prismarine-template": "file:.", - "standard": "^16.0.1" - } + "name": "web_client", + "private": true, + "version": "1.0.0", + "description": "web_client", + "main": "index.js", + "scripts": { + "prepare": "webpack", + "start": "webpack serve", + "prod-start": "node server.js", + "build-start": "npm run prepare && npm run prod-start", + "lint": "standard", + "fix": "standard --fix", + "test": "npm run lint && mocha" + }, + "dependencies": { + "assert": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "clean-webpack-plugin": "^3.0.0", + "compression": "^1.7.4", + "constants-browserify": "^1.0.0", + "copy-webpack-plugin": "^7.0.0", + "crypto-browserify": "^3.12.0", + "events": "^3.2.0", + "express": "^4.17.1", + "http-browserify": "^1.7.0", + "https-browserify": "^1.0.0", + "memfs": "^3.2.0", + "mineflayer": "^2.39.2", + "net-browserify": "^0.2.4", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "prismarine-viewer": "^1.14.0", + "process": "^0.11.10", + "request": "^2.88.2", + "stream-browserify": "^3.0.0", + "three": "^0.124.0", + "timers-browserify": "^2.0.12", + "webpack": "^5.11.0", + "webpack-cli": "^4.2.0", + "webpack-dev-server": "^3.11.0" + }, + "devDependencies": { + "http-server": "^0.12.3", + "lodash-webpack-plugin": "^0.11.6", + "mocha": "^8.3.0", + "standard": "^16.0.3" + } } diff --git a/perf_hooks_replacement.js b/perf_hooks_replacement.js new file mode 100644 index 0000000..69b0e2e --- /dev/null +++ b/perf_hooks_replacement.js @@ -0,0 +1 @@ +module.exports.performance = window.performance diff --git a/server.js b/server.js new file mode 100644 index 0000000..0163072 --- /dev/null +++ b/server.js @@ -0,0 +1,76 @@ +const express = require('express') +const netApi = require('net-browserify') +const bodyParser = require('body-parser') +const request = require('request') +const compression = require('compression') + +// Create our app +const app = express() + +app.use(function (req, res, next) { + res.header('Access-Control-Allow-Origin', req.get('Origin') || '*') + res.header('Access-Control-Allow-Credentials', 'true') + res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE') + res.header('Access-Control-Expose-Headers', 'Content-Length') + res.header( + 'Access-Control-Allow-Headers', + 'Accept, Authorization, Content-Type, X-Requested-With, Range' + ) + if (req.method === 'OPTIONS') { + return res.send(200) + } else { + return next() + } +}) + +app.use(compression()) +app.use(netApi()) +app.use(express.static('./public')) + +app.use(bodyParser.json({ limit: '100kb' })) + +app.all('*', function (req, res, next) { + // Set CORS headers: allow all origins, methods, and headers: you may want to lock this down in a production environment + res.header('Access-Control-Allow-Origin', '*') + res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE') + res.header( + 'Access-Control-Allow-Headers', + req.header('access-control-request-headers') + ) + + if (req.method === 'OPTIONS') { + // CORS Preflight + res.send() + } else { + const targetURL = req.header('Target-URL') + if (!targetURL) { + res.status(404).send({ error: '404 Not Found' }) + return + } + const newHeaders = req.headers + newHeaders.host = targetURL + .replace('https://', '') + .replace('http://', '') + .split('/')[0] + request( + { + url: targetURL + req.url, + method: req.method, + json: req.body, + headers: req.headers + }, + function (error, response, body) { + if (error) { + console.error(error) + console.error('error: ' + response.statusCode) + } + // console.log(body); + } + ).pipe(res) + } +}) + +// Start the server +const server = app.listen(8080, function () { + console.log('Server listening on port ' + server.address().port) +}) diff --git a/test/basic.test.js b/test/basic.test.js index 968ec21..d461258 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -1,7 +1,7 @@ -/* eslint-env jest */ +/* eslint-env mocha */ describe('basic', () => { - test('test', () => { + it('test', () => { }) }) diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..12b1fa2 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,82 @@ +const webpack = require('webpack') +const path = require('path') +const LodashModuleReplacementPlugin = require('lodash-webpack-plugin') +const CopyPlugin = require('copy-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') + +const config = { + // devtool: 'inline-source-map', + // mode: 'development', + mode: 'production', + entry: path.resolve(__dirname, './index.js'), + output: { + path: path.resolve(__dirname, './public'), + filename: './index.js' + }, + resolve: { + alias: { + 'minecraft-protocol': path.resolve( + __dirname, + 'node_modules/minecraft-protocol/src/index.js' + ), // Hack to allow creating the client in a browser + express: false, + net: 'net-browserify', + fs: 'memfs' + }, + fallback: { + zlib: require.resolve('browserify-zlib'), + stream: require.resolve('stream-browserify'), + buffer: require.resolve('buffer/'), + events: require.resolve('events/'), + assert: require.resolve('assert/'), + crypto: require.resolve('crypto-browserify'), + path: require.resolve('path-browserify'), + constants: require.resolve('constants-browserify'), + os: require.resolve('os-browserify/browser'), + http: require.resolve('http-browserify'), + https: require.resolve('https-browserify'), + timers: require.resolve('timers-browserify'), + // fs: require.resolve("fs-memory/singleton"), + child_process: false, + perf_hooks: path.resolve(__dirname, 'perf_hooks_replacement.js'), + dns: path.resolve(__dirname, 'dns.js') + } + }, + plugins: [ + // fix "process is not defined" error: + new CleanWebpackPlugin(), + new webpack.ProvidePlugin({ + process: 'process/browser' + }), + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'] + }), + new webpack.NormalModuleReplacementPlugin( + /prismarine-viewer[/|\\]viewer[/|\\]lib[/|\\]utils/, + './utils.web.js' + ), + new CopyPlugin({ + patterns: [ + { from: 'index.html', to: './index.html' }, + { from: 'node_modules/prismarine-viewer/public/blocksStates/', to: './blocksStates/' }, + { from: 'node_modules/prismarine-viewer/public/textures/', to: './textures/' }, + { from: 'node_modules/prismarine-viewer/public/worker.js', to: './' }, + { from: 'node_modules/prismarine-viewer/public/supportedVersions.json', to: './' } + ] + }), + new webpack.optimize.ModuleConcatenationPlugin(), + new LodashModuleReplacementPlugin() + ], + devServer: { + contentBase: path.resolve(__dirname, './public'), + compress: true, + inline: true, + // open: true, + hot: true, + watchOptions: { + ignored: /node_modules/ + } + } +} + +module.exports = config