diff --git a/.dockerignore b/.dockerignore
index 285d1303..38ca0016 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,3 @@
-# we dont want default config to be loaded in the dockerfile, but rather using a volume
-config.json
# build stuff
node_modules
public
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
index 98388260..a91015d2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -96,5 +96,22 @@
"unicorn/filename-case": "off",
"max-depth": "off"
},
+ "overrides": [
+ {
+ "files": [
+ "*.js"
+ ],
+ "rules": {
+ "space-before-function-paren": [
+ "error",
+ {
+ "anonymous": "always",
+ "named": "never",
+ "asyncArrow": "always"
+ }
+ ]
+ }
+ }
+ ],
"root": true
}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0663fce7..22c95b87 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,6 +8,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@master
+ - name: Setup Java JDK
+ uses: actions/setup-java@v1.4.3
+ with:
+ java-version: 17
+ java-package: jre
- name: Install pnpm
run: npm i -g pnpm@9.0.4
- uses: actions/setup-node@v4
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 18c1a9bf..fc3f2f12 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,4 +1,4 @@
-name: Deploy to GitHub pages
+name: Release
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
@@ -17,6 +17,7 @@ jobs:
# - run: pnpm install
# - run: pnpm build
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
+ - run: node scripts/replaceFavicon.mjs ${{ secrets.FAVICON_MAIN }}
# will install + build to .vercel/output/static
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
- run: pnpm build-storybook
diff --git a/.gitignore b/.gitignore
index 240b751a..3a188862 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,5 +17,6 @@ out
.vercel
generated
storybook-static
+server-jar
src/react/npmReactComponents.ts
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index a8a219f1..05c36eba 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -1,9 +1,10 @@
import React from 'react'
-import type { Preview } from "@storybook/react";
+import type { Preview } from "@storybook/react"
-import '../src/styles.css'
import './storybook.css'
+import '../src/styles.css'
+import '../src/scaleInterface'
const preview: Preview = {
decorators: [
@@ -11,7 +12,7 @@ const preview: Preview = {
const noScaling = c.parameters.noScaling
return
-
;
+
},
],
parameters: {
@@ -23,6 +24,6 @@ const preview: Preview = {
},
},
},
-};
+}
-export default preview;
+export default preview
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 253b2a52..eaf77b66 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -21,7 +21,7 @@ How different modules are used:
- `mineflayer` - provider `bot` variable and as mineflayer states it is a wrapper for the `node-minecraft-protocol` module and is used to connect and interact with real Java Minecraft servers. However not all events & properties are exposed and sometimes you have to use `bot._client.on('packet_name', data => ...)` to handle packets that are not handled via mineflayer API. Also you can use almost any mineflayer plugin.
-## Making protocol changes
+## Making protocol-related changes
You can get a description of packets for the latest protocol version from and for previous protocol versions from (look for *Page* links that have *Protocol* in URL).
@@ -37,6 +37,7 @@ Also there are [src/generatedClientPackets.ts](src/generatedClientPackets.ts) an
- Some data are cached between restarts. If you see something doesn't work after upgrading dependencies, try to clear the by simply removing the `dist` folder.
- The same folder `dist` is used for both development and production builds, so be careful when deploying the project.
- Use `start-prod` script to start the project in production mode after running the `build` script to build the project.
+- If CI is failing on the next branch for some reason, feel free to use the latest commit for release branch. We will update the base branch asap. Please, always make sure to allow maintainers do changes when opening PRs.
### Would be useful to have
diff --git a/Dockerfile b/Dockerfile
index aa9eb3dc..086240b5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,22 @@
-FROM node:14-alpine
+FROM node:18-alpine
# Without git installing the npm packages fails
RUN apk add git
RUN mkdir /app
WORKDIR /app
COPY . /app
-RUN npm install
-RUN npm run build
+# install python and other dependencies
+RUN apk add python3 make g++ cairo-dev pango-dev jpeg-dev giflib-dev librsvg-dev
+# install pnpm
+RUN npm i -g pnpm@9.0.4
+RUN pnpm install
+# only for prod
+RUN pnpm run build
+# ---
+EXPOSE 8080
+# uncomment for development
+# EXPOSE 9090
+# VOLUME /app/src
+# VOLUME /app/prismarine-viewer
+# ENTRYPOINT ["pnpm", "run", "run-all"]
+# only for prod
ENTRYPOINT ["npm", "run", "prod-start"]
diff --git a/README.MD b/README.MD
index ce368540..9a4ba24a 100644
--- a/README.MD
+++ b/README.MD
@@ -4,19 +4,20 @@
A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using modern web technologies.
-If you encounter any bugs or usability issues, please report them!
+If you encounter any bugs or usability issues, please report them! For development, see [development](#development--debugging).
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link) [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the `develop` (default) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) - so it's usually newer, but might be less stable.
### Big Features
- Open any zip world file or even folder in read-write mode!
-- Connect to cracked servers* (it's possible because of proxy servers, see below)
+- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
- Singleplayer mode with simple world generations!
- Works offline
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
- First-class touch (mobile) & controller support
- Resource pack support
+- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
- even even more!
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
@@ -34,11 +35,13 @@ Zip files and folders are supported. Just drag and drop them into the browser wi
In case of opening zip files they are stored in your ram entirely, so there is a ~300mb file limit on IOS.
Whatever offline mode you used (zip, folder, just single player), you can always export world with the `/export` command typed in the game chat.
+
+
### Servers
-You can play almost on any server, supporting offline connections.
+You can play almost on any Java server, vanilla servers are fully supported.
See the [Mineflayer](https://github.com/PrismarineJS/mineflayer) repo for the list of supported versions (should support majority of versions).
-There is a builtin proxy, but you can also host a your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
+There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
MS account authentication will be supported soon.
### Rendering
@@ -46,8 +49,9 @@ MS account authentication will be supported soon.
#### Three.js Renderer
- Uses WebGL2. Chunks are rendered using Geometry Buffers prepared by 4 mesher workers.
-- Supports FXAA
-- Doesn't support culling
+- Entities & text rendering
+- Supports resource packs
+- Doesn't support occlusion culling
@@ -63,9 +67,9 @@ There are many many settings, that are not exposed in the UI yet. You can find o
To open the console, press `F12`, or if you are on mobile, you can type `#debug` in the URL (browser address bar), it wont't reload the page, but you will see a button to open the console. This way you can change advanced settings and see all errors or warnings. Also this way you can access global variables (described below).
-### Debugging
+### Development & Debugging
-It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info.
+It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. Also you can look at Dockerfile for reference.
There is world renderer playground ([link](https://mcon.vercel.app/playground.html)).
@@ -109,10 +113,10 @@ Press `Y` to set query parameters to url of your current game state.
- `?version=` - Set the version for server
- `?lockConnect=true` - Disable cancel / save buttons, useful for integrates iframes
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
-
- `?loadSave=` - Load the save on load with the specified folder name (not title)
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
- `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work
+
- `?map=` - Load the map from ZIP. You can use any url, but it must be CORS enabled.
- `?setting=:` - Set the and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
@@ -120,9 +124,13 @@ Press `Y` to set query parameters to url of your current game state.
### Notable Things that Power this Project
- [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked
-- [Flying Squid](https://github.com/prismarineJS/flying-squid) - The builtin server that makes single player possible! Here forked version is used.
+- [Forked Flying Squid (Space Squid)](https://github.com/zardoy/space-squid) - The builtin offline server that makes single player & P2P possible!
- [Prismarine Provider Anvil](https://github.com/PrismarineJS/prismarine-provider-anvil) - Handles world loading (region format)
- [Prismarine Physics](https://github.com/PrismarineJS/prismarine-physics) - Does all the physics calculations
- [Minecraft Protocol](https://github.com/PrismarineJS/node-minecraft-protocol) - Makes connections to servers possible
- [Peer.js](https://peerjs.com/) - P2P networking (when you open to wan)
- [Three.js](https://threejs.org/) - Helping in 3D rendering
+
+### Alternatives
+
+- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
diff --git a/README.NPM.MD b/README.NPM.MD
index 24c90bc9..c44492c6 100644
--- a/README.NPM.MD
+++ b/README.NPM.MD
@@ -1,9 +1,13 @@
# Minecraft React
+Minecraft UI components for React.
+
```bash
-yarn add minecraft-react
+pnpm i minecraft-react
```
+
+
## Usage
```jsx
@@ -24,7 +28,7 @@ const App = () => {
}
```
-See [Storybook](https://mcraft.fun/storybook/) or [Storybook (Mirror link)](https://mcon.vercel.app/storybook/) for more examples and full components list. Also take a look at the full [standalone example](https://github.com/zardoy/prismarine-web-client/tree/experiments/UiStandaloneExample.tsx).
+See [Storybook](https://mcraft.fun/storybook/) or [Storybook (Mirror link)](https://mcon.vercel.app/storybook/) for more examples and full components list. Also take a look at the full [standalone example](https://github.com/zardoy/minecraft-web-client/tree/experiments/UiStandaloneExample.tsx).
There are two types of components:
diff --git a/assets/extra-textures/loading.png b/assets/extra-textures/loading.png
deleted file mode 100644
index 4f6a121a..00000000
Binary files a/assets/extra-textures/loading.png and /dev/null differ
diff --git a/assets/favicon.png b/assets/favicon.png
index 4f0db721..046cacd0 100644
Binary files a/assets/favicon.png and b/assets/favicon.png differ
diff --git a/config.json b/config.json
index 1bbbfd47..e4f86060 100644
--- a/config.json
+++ b/config.json
@@ -15,9 +15,9 @@
"description": "One of the best servers here. Join now!"
},
{
- "ip": "play.minemalia.com",
+ "ip": "sus.shhnowisnottheti.me",
"version": "1.18.2",
- "description": "Only login with existing accounts."
+ "description": "Creative, your own 'boxes' (islands)"
}
]
}
diff --git a/cypress/e2e/index.spec.ts b/cypress/e2e/index.spec.ts
index 16425366..b0d7118b 100644
--- a/cypress/e2e/index.spec.ts
+++ b/cypress/e2e/index.spec.ts
@@ -1,4 +1,6 @@
+/* eslint-disable max-nested-callbacks */
///
+import supportedVersions from '../../src/supportedVersions.mjs'
import { setOptions, cleanVisit, visit } from './shared'
// todo use ssl
@@ -12,7 +14,7 @@ const compareRenderedFlatWorld = () => {
}
const testWorldLoad = () => {
- cy.document().then({ timeout: 20_000 }, doc => {
+ return cy.document().then({ timeout: 25_000 }, doc => {
return new Cypress.Promise(resolve => {
doc.addEventListener('cypress-world-ready', resolve)
})
@@ -22,7 +24,7 @@ const testWorldLoad = () => {
}
it('Loads & renders singleplayer', () => {
- visit('/?singleplayer=1')
+ cleanVisit('/?singleplayer=1')
setOptions({
localServerOptions: {
generation: {
@@ -36,7 +38,7 @@ it('Loads & renders singleplayer', () => {
testWorldLoad()
})
-it('Joins to server', () => {
+it('Joins to local flying-squid server', () => {
visit('/?ip=localhost&version=1.16.1')
window.localStorage.version = ''
// todo replace with data-test
@@ -47,6 +49,45 @@ it('Joins to server', () => {
testWorldLoad()
})
+it('Joins to local latest Java vanilla server', () => {
+ const version = supportedVersions.at(-1)!
+ cy.task('startServer', [version, 25_590]).then(() => {
+ visit('/?ip=localhost:25590&username=bot')
+ cy.get('[data-test-id="connect-qs"]').click()
+ testWorldLoad().then(() => {
+ let x = 0
+ let z = 0
+ cy.window().then((win) => {
+ x = win.bot.entity.position.x
+ z = win.bot.entity.position.z
+ })
+ cy.document().trigger('keydown', { code: 'KeyW' })
+ cy.wait(1500).then(() => {
+ cy.document().trigger('keyup', { code: 'KeyW' })
+ cy.window().then(async (win) => {
+ // eslint-disable-next-line prefer-destructuring
+ const bot: typeof __type_bot = win.bot
+ // todo use f3 stats instead
+ if (bot.entity.position.x === x && bot.entity.position.z === z) {
+ throw new Error('Player not moved')
+ }
+
+ bot.chat('Hello') // todo assert
+ bot.chat('/gamemode creative')
+ // bot.on('message', () => {
+ void bot.creative.setInventorySlot(bot.inventory.hotbarStart, new win.PrismarineItem(1, 1, 0))
+ // })
+ await bot.lookAt(bot.entity.position.offset(1, 0, 1))
+ }).then(() => {
+ cy.document().trigger('mousedown', { button: 2, isTrusted: true, force: true }) // right click
+ cy.document().trigger('mouseup', { button: 2, isTrusted: true, force: true })
+ cy.wait(1000)
+ })
+ })
+ })
+ })
+})
+
it('Loads & renders zip world', () => {
cleanVisit()
cy.get('[data-test-id="select-file-folder"]').click({ shiftKey: true })
@@ -54,9 +95,14 @@ it('Loads & renders zip world', () => {
testWorldLoad()
})
-it.skip('Performance test', () => {
- // select that world
- // from -2 85 24
- // await bot.loadPlugin(pathfinder.pathfinder)
- // bot.pathfinder.goto(new pathfinder.goals.GoalXZ(28, -28))
+
+it.skip('Loads & renders world from folder', () => {
+ cleanVisit()
+ // dragndrop folder
+ cy.get('[data-test-id="select-file-folder"]').click()
+ cy.get('input[type="file"]').selectFile('server-jar/world', {
+ force: true,
+ // action: 'drag-drop',
+ })
+ testWorldLoad()
})
diff --git a/cypress/e2e/shared.ts b/cypress/e2e/shared.ts
index 9292a8d5..47518f1b 100644
--- a/cypress/e2e/shared.ts
+++ b/cypress/e2e/shared.ts
@@ -3,6 +3,9 @@ import { AppOptions } from '../../src/optionsStorage'
export const cleanVisit = (url?) => {
cy.clearLocalStorage()
visit(url)
+ window.localStorage.options = {
+ chatOpacity: 0
+ }
}
export const visit = (url = '/') => {
window.localStorage.cypress = 'true'
diff --git a/cypress/minecraft-server.mjs b/cypress/minecraft-server.mjs
index 18f4e3db..32be0c9d 100644
--- a/cypress/minecraft-server.mjs
+++ b/cypress/minecraft-server.mjs
@@ -2,6 +2,7 @@
import mcServer from 'flying-squid'
import defaultOptions from 'flying-squid/config/default-settings.json' assert { type: 'json' }
+/** @type {Options} */
const serverOptions = {
...defaultOptions,
'online-mode': false,
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 35dc2989..e55f5d26 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -2,11 +2,13 @@
const { cypressEsbuildPreprocessor } = require('cypress-esbuild-preprocessor')
const { initPlugin } = require('cypress-plugin-snapshots/plugin')
const polyfill = require('esbuild-plugin-polyfill-node')
+const { startMinecraftServer } = require('./startServer')
module.exports = (on, config) => {
initPlugin(on, config)
on('file:preprocessor', cypressEsbuildPreprocessor({
esbuildOptions: {
+ sourcemap: true,
plugins: [
polyfill.polyfillNode({
polyfills: {
@@ -17,10 +19,15 @@ module.exports = (on, config) => {
},
}))
on('task', {
- log (message) {
+ log(message) {
console.log(message)
return null
},
})
+ on('task', {
+ async startServer([version, port]) {
+ return startMinecraftServer(version, port)
+ }
+ })
return config
}
diff --git a/cypress/plugins/ops.json b/cypress/plugins/ops.json
new file mode 100644
index 00000000..ade5aaa5
--- /dev/null
+++ b/cypress/plugins/ops.json
@@ -0,0 +1,8 @@
+[
+ {
+ "uuid": "67128b5b-2e6b-3ad1-baa0-1b937b03e5c5",
+ "name": "bot",
+ "level": 4,
+ "bypassesPlayerLimit": false
+ }
+]
diff --git a/cypress/plugins/server.properties b/cypress/plugins/server.properties
new file mode 100644
index 00000000..5873a1aa
--- /dev/null
+++ b/cypress/plugins/server.properties
@@ -0,0 +1,61 @@
+#Minecraft server properties
+allow-flight=false
+allow-nether=true
+broadcast-console-to-ops=true
+broadcast-rcon-to-ops=true
+difficulty=peaceful
+enable-command-block=false
+enable-jmx-monitoring=false
+enable-query=false
+enable-rcon=false
+enable-status=true
+enforce-secure-profile=true
+enforce-whitelist=false
+entity-broadcast-range-percentage=100
+force-gamemode=false
+function-permission-level=2
+gamemode=survival
+generate-structures=true
+generator-settings={}
+hardcore=false
+hide-online-players=false
+initial-disabled-packs=
+initial-enabled-packs=vanilla
+level-name=world
+level-seed=
+level-type=flat
+log-ips=true
+max-build-height=256
+max-chained-neighbor-updates=1000000
+max-players=20
+max-tick-time=60000
+max-world-size=29999984
+motd=A Minecraft Server
+network-compression-threshold=256
+online-mode=false
+op-permission-level=4
+player-idle-timeout=0
+prevent-proxy-connections=false
+pvp=true
+query.port=25565
+rate-limit=0
+rcon.password=
+rcon.port=25575
+require-resource-pack=false
+resource-pack=
+resource-pack-id=
+resource-pack-prompt=
+resource-pack-sha1=
+server-ip=
+server-port=25565
+simulation-distance=10
+snooper-enabled=true
+spawn-animals=true
+spawn-monsters=true
+spawn-npcs=true
+spawn-protection=16
+sync-chunk-writes=true
+text-filtering-config=
+use-native-transport=true
+view-distance=10
+white-list=false
diff --git a/cypress/plugins/startServer.ts b/cypress/plugins/startServer.ts
new file mode 100644
index 00000000..ecf0d210
--- /dev/null
+++ b/cypress/plugins/startServer.ts
@@ -0,0 +1,45 @@
+import { ChildProcess, spawn } from 'child_process'
+import * as fs from 'fs'
+import * as path from 'path'
+import { promisify } from 'util'
+import { downloadServer } from 'minecraft-wrap'
+import * as waitOn from 'wait-on'
+
+let prevProcess: ChildProcess | null = null
+export const startMinecraftServer = async (version: string, port: number) => {
+ if (prevProcess) return null
+ const jar = `./server-jar/${version}.jar`
+
+ const start = () => {
+ // if (prevProcess) {
+ // prevProcess.kill()
+ // }
+
+ prevProcess = spawn('java', ['-jar', path.basename(jar), 'nogui', '--port', `${port}`], {
+ stdio: 'inherit',
+ cwd: path.dirname(jar),
+ })
+ }
+
+ let coldStart = false
+ if (fs.existsSync(jar)) {
+ start()
+ } else {
+ coldStart = true
+ promisify(downloadServer)(version, jar).then(() => {
+ // add eula.txt
+ fs.writeFileSync(path.join(path.dirname(jar), 'eula.txt'), 'eula=true')
+ // copy cypress/plugins/server.properties
+ fs.copyFileSync(path.join(__dirname, 'server.properties'), path.join(path.dirname(jar), 'server.properties'))
+ // copy ops.json
+ fs.copyFileSync(path.join(__dirname, 'ops.json'), path.join(path.dirname(jar), 'ops.json'))
+ start()
+ })
+ }
+
+ return new Promise((res) => {
+ waitOn({ resources: [`tcp:localhost:${port}`] }, () => {
+ setTimeout(() => res(null), coldStart ? 6500 : 2000) // todo retry instead of timeout
+ })
+ })
+}
diff --git a/docs-assets/singleplayer-future-city-1-10-2.jpg b/docs-assets/singleplayer-future-city-1-10-2.jpg
new file mode 100644
index 00000000..e5be2ada
Binary files /dev/null and b/docs-assets/singleplayer-future-city-1-10-2.jpg differ
diff --git a/esbuild.mjs b/esbuild.mjs
index 8cb7b4bd..e7a964b8 100644
--- a/esbuild.mjs
+++ b/esbuild.mjs
@@ -60,6 +60,7 @@ const buildOptions = {
net: 'net-browserify',
assert: 'assert',
dns: './src/dns.js',
+ 'yggdrasil': './src/yggdrasilReplacement.ts',
// todo write advancedAliases plugin
},
inject: [
@@ -79,11 +80,15 @@ const buildOptions = {
loader: {
// todo use external or resolve issues with duplicating
'.png': 'dataurl',
+ '.svg': 'dataurl',
'.map': 'empty',
'.vert': 'text',
'.frag': 'text',
'.wgsl': 'text',
'.obj': 'text',
+ '.woff': 'dataurl',
+ '.woff2': 'dataurl',
+ '.ttf': 'dataurl',
},
write: false,
// todo would be better to enable?
diff --git a/index.html b/index.html
index 3e921855..62e109cd 100644
--- a/index.html
+++ b/index.html
@@ -14,7 +14,7 @@
Loading...
-
A true Minecraft client in your browser!
+
A true Minecraft client in your browser!
`
@@ -94,9 +94,7 @@
-
+