From d978f5c7627bd291135b3b4b71ffd1095bc45df7 Mon Sep 17 00:00:00 2001
From: Romain Beaumont <romain.rom1@gmail.com>
Date: Sat, 27 Feb 2021 22:12:11 +0000
Subject: [PATCH] add code

---
 .gitignore                |   3 +-
 README.md                 |  38 +++++++----
 dns.js                    |  39 +++++++++++
 example.js                |   3 -
 index.html                |  30 +++++++++
 index.js                  | 137 ++++++++++++++++++++++++++++++++++++--
 package.json              |  77 +++++++++++++--------
 perf_hooks_replacement.js |   1 +
 server.js                 |  76 +++++++++++++++++++++
 test/basic.test.js        |   4 +-
 webpack.config.js         |  82 +++++++++++++++++++++++
 11 files changed, 434 insertions(+), 56 deletions(-)
 create mode 100644 dns.js
 delete mode 100644 example.js
 create mode 100644 index.html
 create mode 100644 perf_hooks_replacement.js
 create mode 100644 server.js
 create mode 100644 webpack.config.js

diff --git a/.gitignore b/.gitignore
index 3ff87f131..a0031ecff 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 320d7c467..c334a3435 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.
 
-```js
-const template = require('prismarine-template')
+## Features
+
+* display blocks
+* display entities as colored rectangles
+* movement sync
+
+## Roadmap
+
+* chat
+* block placing and breaking
 
-template.helloWorld()
+## Run
+
+```js
+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 000000000..be7206ae3
--- /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 ceadd46e3..000000000
--- 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 000000000..beddd6437
--- /dev/null
+++ b/index.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Prismarine Viewer</title>
+    <style type="text/css">
+      html {
+        overflow: hidden;
+      }
+
+      html, body {
+        height: 100%;
+
+        margin: 0;
+        padding: 0;
+      }
+
+      canvas {
+        height: 100%;
+        width: 100%;
+        font-size: 0;
+
+        margin: 0;
+        padding: 0;
+      }
+    </style>
+  </head>
+  <body>
+    <script type="text/javascript" src="index.js"></script>
+  </body>
+</html>
diff --git a/index.js b/index.js
index 4bd4c7509..23ee193af 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 */
+
+// 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)
 
-module.exports.helloWorld = function () {
-  console.log('Hello world !')
+    // 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 3f1a05d8b..50f0e3a1c 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 000000000..69b0e2ed5
--- /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 000000000..0163072c5
--- /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 968ec21f8..d461258c8 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 000000000..12b1fa2b4
--- /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