Skip to content

Commit 25276a6

Browse files
authored
feat: peerjs (p2p) wan multiplayer! (#18)
2 parents 111188d + b4952e1 commit 25276a6

File tree

14 files changed

+316
-32
lines changed

14 files changed

+316
-32
lines changed

.github/workflows/preview.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
fetch-depth: 0
1717
- name: Install Global Dependencies
1818
run: npm install --global vercel pnpm
19+
- run: pnpm install
20+
- run: pnpm run build
1921
- uses: amondnet/vercel-action@v25
2022
with:
2123
vercel-token: ${{ secrets.VERCEL_TOKEN }}

package.json

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@
1919
],
2020
"author": "PrismarineJS",
2121
"license": "MIT",
22-
"release": {
23-
"initialVersion": {
24-
"version": "0.1.0",
25-
"releaseNotes": "The start of something new...",
26-
"releaseNotesWithExisting": "The start of something new..."
27-
}
28-
},
2922
"dependencies": {
3023
"@dimaka/interface": "0.0.1",
3124
"@emotion/css": "^11.11.2",
@@ -51,8 +44,10 @@
5144
"lit": "^2.8.0",
5245
"minecraft-data": "^3.0.0",
5346
"net-browserify": "github:PrismarineJS/net-browserify",
47+
"peerjs": "^1.5.0",
5448
"pretty-bytes": "^6.1.1",
5549
"prismarine-world": "^3.6.2",
50+
"qrcode.react": "^3.1.0",
5651
"querystring": "^0.2.1",
5752
"react": "^18.2.0",
5853
"react-dom": "^18.2.0",
@@ -77,6 +72,7 @@
7772
"cypress": "^9.5.4",
7873
"cypress-esbuild-preprocessor": "^1.0.2",
7974
"events": "^3.3.0",
75+
"filesize": "^10.0.12",
8076
"html-webpack-plugin": "^5.5.3",
8177
"http-browserify": "^1.7.0",
8278
"http-server": "^14.1.1",
@@ -97,7 +93,6 @@
9793
"three": "0.128.0",
9894
"timers-browserify": "^2.0.12",
9995
"url-loader": "^4.1.1",
100-
"filesize": "^10.0.12",
10196
"use-typed-event-listener": "^4.0.2",
10297
"vite": "^4.4.9",
10398
"webpack": "^5.88.2",

src/builtinCommands.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import JSZip from 'jszip'
22
import fs from 'fs'
33
import { join } from 'path'
44
import { fsState } from './loadSave'
5+
import { closeWan, openToWanAndCopyJoinLink } from './localServerMultiplayer'
56

67
const notImplemented = () => {
78
return 'Not implemented yet'
@@ -50,15 +51,30 @@ const exportWorld = async () => {
5051

5152
window.exportWorld = exportWorld
5253

54+
const writeText = (text) => {
55+
bot._client.emit('chat', {
56+
message: JSON.stringify({ text })
57+
})
58+
}
59+
5360
const commands = [
5461
{
5562
command: ['/download', '/export'],
5663
invoke: exportWorld
5764
},
5865
{
59-
command: ['/publish'],
60-
// todo
61-
invoke: notImplemented
66+
command: ['/publish', '/share'],
67+
invoke: async () => {
68+
const text = await openToWanAndCopyJoinLink(writeText)
69+
if (text) writeText(text)
70+
}
71+
},
72+
{
73+
command: ['/close'],
74+
invoke: () => {
75+
const text = closeWan()
76+
if (text) writeText(text)
77+
}
6278
},
6379
{
6480
command: '/reset-world -y',

src/customServer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import EventEmitter from 'events'
22

33
import Client from 'minecraft-protocol/src/client'
44

5+
window.serverDataChannel ??= {}
56
export const customCommunication = {
67
sendData(data) {
78
//@ts-ignore

src/defaultLocalServerOptions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'header': 'Flying squid',
3030
'footer': 'Test server'
3131
},
32+
keepAlive: false,
3233
'everybody-op': true,
3334
'max-entities': 100,
3435
'version': '1.14.4',

src/downloadAndOpenWorld.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const getConstantFilesize = (bytes: number) => {
66
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
77
}
88

9+
export const hasMapUrl = () => {
10+
const qs = new URLSearchParams(window.location.search)
11+
const mapUrl = qs.get('map')
12+
return !!mapUrl
13+
}
14+
915
export default async () => {
1016
const qs = new URLSearchParams(window.location.search)
1117
const mapUrl = qs.get('map')

src/globalState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,11 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY })
103103
// ---
104104

105105
export const miscUiState = proxy({
106+
currentDisplayQr: null as string | null,
106107
currentTouch: null as boolean | null,
107108
singleplayer: false,
109+
flyingSquid: false,
110+
wanOpened: false,
108111
gameLoaded: false,
109112
resourcePackInstalled: false,
110113
})
@@ -148,6 +151,7 @@ window.addEventListener('beforeunload', (event) => {
148151
// todo-low maybe exclude chat?
149152
if (!isGameActive(true) && activeModalStack.at(-1)?.elem.id !== 'chat') return
150153
if (sessionStorage.lastReload && options.preventDevReloadWhilePlaying === false) return
154+
if (options.closeConfirmation === false) return
151155

152156
// For major browsers doning only this is enough
153157
event.preventDefault()

src/index.ts

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import './controls'
3131
import './dragndrop'
3232
import './browserfs'
3333
import './eruda'
34-
import downloadAndOpenWorld from './downloadAndOpenWorld'
34+
import downloadAndOpenWorld, { hasMapUrl } from './downloadAndOpenWorld'
3535

3636
import net from 'net'
3737
import Stats from 'stats.js'
@@ -63,7 +63,8 @@ import {
6363
isCypress,
6464
loadScript,
6565
toMajorVersion,
66-
setLoadingScreenStatus
66+
setLoadingScreenStatus,
67+
resolveTimeout
6768
} from './utils'
6869

6970
import {
@@ -74,13 +75,14 @@ import {
7475

7576
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
7677
import serverOptions from './defaultLocalServerOptions'
77-
import { customCommunication } from './customServer'
78+
import { clientDuplex, customCommunication } from './customServer'
7879
import updateTime from './updateTime'
7980
import { options } from './optionsStorage'
8081
import { subscribeKey } from 'valtio/utils'
8182
import _ from 'lodash'
8283
import { contro } from './controls'
8384
import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack'
85+
import { connectToPeer } from './localServerMultiplayer'
8486

8587
//@ts-ignore
8688
window.THREE = THREE
@@ -263,16 +265,17 @@ const removeAllListeners = () => {
263265
disposables = []
264266
}
265267

266-
/**
267-
* @param {{ server: any; port?: string; singleplayer: any; username: any; password: any; proxy: any; botVersion?: any; serverOverrides? }} connectOptions
268-
*/
269-
async function connect(connectOptions) {
268+
async function connect(connectOptions: {
269+
server: any; port?: string; singleplayer?: any; username: any; password: any; proxy: any; botVersion?: any; serverOverrides?; peerId?: string
270+
}) {
270271
const menu = document.getElementById('play-screen')
271272
menu.style = 'display: none;'
272273
removePanorama()
273274

274275
const singeplayer = connectOptions.singleplayer
276+
const p2pMultiplayer = !!connectOptions.peerId
275277
miscUiState.singleplayer = singeplayer
278+
miscUiState.flyingSquid = singeplayer || p2pMultiplayer
276279
const oldSetInterval = window.setInterval
277280
// @ts-ignore
278281
window.setInterval = (callback, ms) => {
@@ -403,9 +406,9 @@ async function connect(connectOptions) {
403406
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
404407
}
405408

406-
const version = connectOptions.botVersion ?? serverOptions.version
407-
if (version) {
408-
await downloadMcData(version)
409+
const downloadVersion = connectOptions.botVersion || singeplayer ? serverOptions.version : undefined
410+
if (downloadVersion) {
411+
await downloadMcData(downloadVersion)
409412
}
410413

411414
if (singeplayer) {
@@ -421,7 +424,6 @@ async function connect(connectOptions) {
421424
// flying-squid: 'login' -> player.login -> now sends 'login' event to the client (handled in many plugins in mineflayer) -> then 'update_health' is sent which emits 'spawn' in mineflayer
422425

423426
setLoadingScreenStatus('Starting local server')
424-
window.serverDataChannel ??= {}
425427
localServer = window.localServer = startLocalServer()
426428
// todo need just to call quit if started
427429
// loadingScreen.maybeRecoverable = false
@@ -433,16 +435,23 @@ async function connect(connectOptions) {
433435
}
434436
}
435437

438+
const usingCustomCommunication = true
439+
440+
const botDuplex = !p2pMultiplayer ? undefined/* clientDuplex */ : await connectToPeer(connectOptions.peerId);
441+
436442
setLoadingScreenStatus('Creating mineflayer bot')
437443
bot = mineflayer.createBot({
438444
host,
439445
port,
440-
version: connectOptions.botVersion === '' ? false : connectOptions.botVersion,
446+
version: !connectOptions.botVersion ? false : connectOptions.botVersion,
447+
...singeplayer || p2pMultiplayer ? {
448+
keepAlive: false,
449+
stream: botDuplex,
450+
} : {},
441451
...singeplayer ? {
442452
version: serverOptions.version,
443453
connect() { },
444-
keepAlive: false,
445-
customCommunication
454+
customCommunication: usingCustomCommunication ? customCommunication : undefined,
446455
} : {},
447456
username,
448457
password,
@@ -454,7 +463,8 @@ async function connect(connectOptions) {
454463
await downloadMcData(client.version)
455464
}
456465
})
457-
if (singeplayer) {
466+
if (singeplayer || p2pMultiplayer) {
467+
// p2pMultiplayer still uses the same flying-squid server
458468
const _supportFeature = bot.supportFeature
459469
bot.supportFeature = (feature) => {
460470
if (unsupportedLocalServerFeatures.includes(feature)) {
@@ -463,13 +473,16 @@ async function connect(connectOptions) {
463473
return _supportFeature(feature)
464474
}
465475

466-
bot.emit('inject_allowed')
467-
bot._client.emit('connect')
476+
if (usingCustomCommunication) {
477+
bot.emit('inject_allowed')
478+
bot._client.emit('connect')
479+
}
468480
}
469481
} catch (err) {
470482
handleError(err)
471483
}
472484
if (!bot) return
485+
let p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new Error('Spawn timeout. There might be error on other side, check console.') }, 20_000) : undefined
473486
hud.preload(bot)
474487

475488
// bot.on('inject_allowed', () => {
@@ -500,6 +513,7 @@ async function connect(connectOptions) {
500513
})
501514

502515
bot.once('spawn', () => {
516+
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
503517
// todo display notification if not critical
504518
const mcData = require('minecraft-data')(bot.version)
505519

@@ -702,6 +716,7 @@ async function connect(connectOptions) {
702716
errorAbortController.abort()
703717
if (loadingScreen.hasError) return
704718
// remove loading screen, wait a second to make sure a frame has properly rendered
719+
setLoadingScreenStatus(undefined)
705720
hideCurrentScreens()
706721
}, singeplayer ? 0 : 2500)
707722
})
@@ -744,4 +759,24 @@ window.addEventListener('keydown', (e) => {
744759
addPanoramaCubeMap()
745760
showModal(document.getElementById('title-screen'))
746761
main()
747-
downloadAndOpenWorld()
762+
if (hasMapUrl()) {
763+
downloadAndOpenWorld()
764+
} else {
765+
window.addEventListener('hud-ready', (e) => {
766+
// try to connect to peer
767+
const qs = new URLSearchParams(window.location.search)
768+
const peerId = qs.get('connectPeer')
769+
const version = qs.get('peerVersion')
770+
if (peerId) {
771+
let username = options.guestUsername
772+
if (!options.askGuestName) username = prompt('Enter your username', username)
773+
options.guestUsername = username
774+
connect({
775+
server: '', port: '', proxy: '', password: '',
776+
username,
777+
botVersion: version || undefined,
778+
peerId
779+
})
780+
}
781+
})
782+
}

0 commit comments

Comments
 (0)