feat: Performance benchmark!! (#153)

This commit is contained in:
Vitaly 2025-04-07 02:21:37 +03:00 committed by GitHub
commit 0aa4d11bdd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 538 additions and 54 deletions

50
.github/workflows/benchmark.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: Benchmark
on:
issue_comment:
types: [created]
push:
branches:
- perf-test
jobs:
deploy:
runs-on: ubuntu-latest
# if: >-
# github.event.issue.pull_request != '' &&
# (
# contains(github.event.comment.body, '/benchmark')
# )
permissions:
pull-requests: write
steps:
- run: lscpu
- name: Checkout
uses: actions/checkout@v2
# with:
# ref: refs/pull/${{ github.event.issue.number }}/head
- run: npm i -g pnpm@9.0.4
- uses: actions/setup-node@v4
with:
node-version: 18
cache: "pnpm"
- run: pnpm install
- run: pnpm build
- run: nohup pnpm prod-start &
- run: pnpm test:benchmark
id: benchmark
continue-on-error: true
# read benchmark results from stdout
- run: |
if [ -f benchmark.txt ]; then
# Format the benchmark results for GitHub comment
BENCHMARK_RESULT=$(cat benchmark.txt | sed 's/^/- /')
echo "BENCHMARK_RESULT<<EOF" >> $GITHUB_ENV
echo "$BENCHMARK_RESULT" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "BENCHMARK_RESULT=Benchmark failed to run or produce results" >> $GITHUB_ENV
fi
- uses: mshick/add-pr-comment@v2
with:
allow-repeats: true
message: |
Benchmark result: ${{ env.BENCHMARK_RESULT }}

View file

@ -1,5 +1,7 @@
import { defineConfig } from 'cypress' import { defineConfig } from 'cypress'
const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true'
export default defineConfig({ export default defineConfig({
video: false, video: false,
chromeWebSecurity: false, chromeWebSecurity: false,
@ -32,7 +34,7 @@ export default defineConfig({
return require('./cypress/plugins/index.js')(on, config) return require('./cypress/plugins/index.js')(on, config)
}, },
baseUrl: 'http://localhost:8080', baseUrl: 'http://localhost:8080',
specPattern: 'cypress/e2e/**/*.spec.ts', specPattern: !isPerformanceTest ? 'cypress/e2e/smoke.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts',
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'], excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
}, },
}) })

View file

@ -0,0 +1,32 @@
/// <reference types="cypress" />
import { BenchmarkAdapterInfo, getAllInfoLines } from '../../src/benchmarkAdapter'
import { cleanVisit } from './shared'
it('Benchmark rendering performance', () => {
cleanVisit('/?openBenchmark=true&renderDistance=5')
// wait for render end event
return cy.document().then({ timeout: 180_000 }, doc => {
return new Cypress.Promise(resolve => {
cy.log('Waiting for world to load')
doc.addEventListener('cypress-world-ready', resolve)
}).then(() => {
cy.log('World loaded')
})
}).then(() => {
cy.window().then(win => {
const adapter = win.benchmarkAdapter as BenchmarkAdapterInfo
const messages = getAllInfoLines(adapter)
// wait for 10 seconds
cy.wait(10_000)
const messages2 = getAllInfoLines(adapter, true)
for (const message of messages) {
cy.log(message)
}
for (const message of messages2) {
cy.log(message)
}
cy.writeFile('benchmark.txt', [...messages, ...messages2].join('\n'))
})
})
})

View file

@ -13,6 +13,7 @@
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts", "prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
"check-build": "pnpm prepare-project && tsc && pnpm build", "check-build": "pnpm prepare-project && tsc && pnpm build",
"test:cypress": "cypress run", "test:cypress": "cypress run",
"test:benchmark": "PERFORMANCE_TEST=true cypress run",
"test:cypress:open": "cypress open", "test:cypress:open": "cypress open",
"test-unit": "vitest", "test-unit": "vitest",
"test:e2e": "start-test http-get://localhost:8080 test:cypress", "test:e2e": "start-test http-get://localhost:8080 test:cypress",
@ -75,7 +76,7 @@
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2", "express": "^4.18.2",
"filesize": "^10.0.12", "filesize": "^10.0.12",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.51", "flying-squid": "npm:@zardoy/flying-squid@^0.0.58",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive", "google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1", "jszip": "^3.10.1",

64
pnpm-lock.yaml generated
View file

@ -120,8 +120,8 @@ importers:
specifier: ^10.0.12 specifier: ^10.0.12
version: 10.0.12 version: 10.0.12
flying-squid: flying-squid:
specifier: npm:@zardoy/flying-squid@^0.0.51 specifier: npm:@zardoy/flying-squid@^0.0.58
version: '@zardoy/flying-squid@0.0.51(encoding@0.1.13)' version: '@zardoy/flying-squid@0.0.58(encoding@0.1.13)'
fs-extra: fs-extra:
specifier: ^11.1.1 specifier: ^11.1.1
version: 11.1.1 version: 11.1.1
@ -438,7 +438,7 @@ importers:
version: 1.3.6 version: 1.3.6
prismarine-block: prismarine-block:
specifier: github:zardoy/prismarine-block#next-era specifier: github:zardoy/prismarine-block#next-era
version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 version: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-chunk: prismarine-chunk:
specifier: github:zardoy/prismarine-chunk#master specifier: github:zardoy/prismarine-chunk#master
version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) version: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
@ -3541,8 +3541,8 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
'@zardoy/flying-squid@0.0.51': '@zardoy/flying-squid@0.0.58':
resolution: {integrity: sha512-HHZ79H9NkS44lL9vk6gVEuJDJqj88gpiBt9Ihh5p4rHXTVbRid95riiNK5dD0kHI94P5/DXdtNalvmJDPU86oQ==} resolution: {integrity: sha512-qkSoaYRpVQaAvcVgZDTe0i4PxaK2l2B6i7GfRCEsyYFl3UaNQYBwwocXqLrIwhsc63bwXa0XQe8UNUubz+A4eA==}
engines: {node: '>=8'} engines: {node: '>=8'}
hasBin: true hasBin: true
@ -6926,6 +6926,11 @@ packages:
version: 1.54.0 version: 1.54.0
engines: {node: '>=22'} engines: {node: '>=22'}
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284:
resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284}
version: 1.57.0
engines: {node: '>=22'}
minecraft-wrap@1.5.1: minecraft-wrap@1.5.1:
resolution: {integrity: sha512-7DZ2WhrcRD3fUMau84l9Va0KWzV92SHNdB7mnNdNhgXID2aW6pjWuYPZi8MepEBemA4XKKdnDx7HmhTbkoiR8A==} resolution: {integrity: sha512-7DZ2WhrcRD3fUMau84l9Va0KWzV92SHNdB7mnNdNhgXID2aW6pjWuYPZi8MepEBemA4XKKdnDx7HmhTbkoiR8A==}
hasBin: true hasBin: true
@ -13652,7 +13657,7 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
'@zardoy/flying-squid@0.0.51(encoding@0.1.13)': '@zardoy/flying-squid@0.0.58(encoding@0.1.13)':
dependencies: dependencies:
'@tootallnate/once': 2.0.0 '@tootallnate/once': 2.0.0
chalk: 5.3.0 chalk: 5.3.0
@ -13663,14 +13668,14 @@ snapshots:
flatmap: 0.0.3 flatmap: 0.0.3
long: 5.2.3 long: 5.2.3
minecraft-data: 3.83.1 minecraft-data: 3.83.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(encoding@0.1.13)
mkdirp: 2.1.6 mkdirp: 2.1.6
node-gzip: 1.1.2 node-gzip: 1.1.2
node-rsa: 1.1.1 node-rsa: 1.1.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
prismarine-entity: 2.3.1 prismarine-entity: 2.3.1
prismarine-item: 1.16.0 prismarine-item: 1.16.0
prismarine-nbt: 2.5.0 prismarine-nbt: 2.7.0
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1) prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1)
prismarine-windows: 2.9.0 prismarine-windows: 2.9.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
@ -17981,6 +17986,31 @@ snapshots:
- encoding - encoding
- supports-color - supports-color
minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/9e116c3dd4682b17c4e2c80249a2447a093d9284(encoding@0.1.13):
dependencies:
'@types/node-rsa': 1.1.4
'@types/readable-stream': 4.0.12
aes-js: 3.1.2
buffer-equal: 1.0.1
debug: 4.4.0(supports-color@8.1.1)
endian-toggle: 0.0.0
lodash.merge: 4.6.2
minecraft-data: 3.83.1
minecraft-folder-path: 1.2.0
node-fetch: 2.7.0(encoding@0.1.13)
node-rsa: 0.4.2
prismarine-auth: 2.4.2(encoding@0.1.13)
prismarine-chat: 1.10.1
prismarine-nbt: 2.7.0
prismarine-realms: 1.3.2(encoding@0.1.13)
protodef: 1.18.0
readable-stream: 4.5.2
uuid-1345: 1.0.2
yggdrasil: 1.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
- supports-color
minecraft-wrap@1.5.1(encoding@0.1.13): minecraft-wrap@1.5.1(encoding@0.1.13):
dependencies: dependencies:
debug: 4.4.0(supports-color@8.1.1) debug: 4.4.0(supports-color@8.1.1)
@ -18043,7 +18073,7 @@ snapshots:
mineflayer-pathfinder@2.4.4: mineflayer-pathfinder@2.4.4:
dependencies: dependencies:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-entity: 2.3.1 prismarine-entity: 2.3.1
prismarine-item: 1.16.0 prismarine-item: 1.16.0
prismarine-nbt: 2.5.0 prismarine-nbt: 2.5.0
@ -18055,7 +18085,7 @@ snapshots:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-chat: 1.10.1 prismarine-chat: 1.10.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
prismarine-entity: 2.3.1 prismarine-entity: 2.3.1
@ -18078,7 +18108,7 @@ snapshots:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-chat: 1.10.1 prismarine-chat: 1.10.1
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
prismarine-entity: 2.3.1 prismarine-entity: 2.3.1
@ -18867,7 +18897,7 @@ snapshots:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9: prismarine-block@https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0):
dependencies: dependencies:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0)
@ -18875,6 +18905,8 @@ snapshots:
prismarine-item: 1.16.0 prismarine-item: 1.16.0
prismarine-nbt: 2.5.0 prismarine-nbt: 2.5.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
transitivePeerDependencies:
- prismarine-registry
prismarine-chat@1.10.1: prismarine-chat@1.10.1:
dependencies: dependencies:
@ -18885,7 +18917,7 @@ snapshots:
prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1): prismarine-chunk@https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1):
dependencies: dependencies:
prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0) prismarine-biome: 1.3.0(minecraft-data@3.83.1)(prismarine-registry@1.11.0)
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-nbt: 2.5.0 prismarine-nbt: 2.5.0
prismarine-registry: 1.11.0 prismarine-registry: 1.11.0
smart-buffer: 4.2.0 smart-buffer: 4.2.0
@ -18923,7 +18955,7 @@ snapshots:
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1): prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1):
dependencies: dependencies:
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1) prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
prismarine-nbt: 2.5.0 prismarine-nbt: 2.5.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
@ -18947,13 +18979,13 @@ snapshots:
prismarine-registry@1.11.0: prismarine-registry@1.11.0:
dependencies: dependencies:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-nbt: 2.7.0 prismarine-nbt: 2.7.0
prismarine-schematic@1.2.3: prismarine-schematic@1.2.3:
dependencies: dependencies:
minecraft-data: 3.83.1 minecraft-data: 3.83.1
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9 prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
prismarine-nbt: 2.5.0 prismarine-nbt: 2.5.0
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
vec3: 0.1.10 vec3: 0.1.10

View file

@ -109,7 +109,9 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
geometryReceiveCount = {} as Record<number, number> geometryReceiveCount = {} as Record<number, number>
allLoadedIn: undefined | number allLoadedIn: undefined | number
onWorldSwitched = [] as Array<() => void> onWorldSwitched = [] as Array<() => void>
renderTimeMax = 0
renderTimeAvg = 0
renderTimeAvgCount = 0
edgeChunks = {} as Record<string, boolean> edgeChunks = {} as Record<string, boolean>
lastAddChunk = null as null | { lastAddChunk = null as null | {
timeout: any timeout: any

View file

@ -50,6 +50,7 @@ export class WorldRendererThree extends WorldRendererCommon {
media: ThreeJsMedia media: ThreeJsMedia
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
camera: THREE.PerspectiveCamera camera: THREE.PerspectiveCamera
renderTimeAvg = 0
get tilesRendered () { get tilesRendered () {
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0) return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
@ -403,6 +404,7 @@ export class WorldRendererThree extends WorldRendererCommon {
} }
render (sizeChanged = false) { render (sizeChanged = false) {
const start = performance.now()
this.lastRendered = performance.now() this.lastRendered = performance.now()
this.cursorBlock.render() this.cursorBlock.render()
@ -427,6 +429,11 @@ export class WorldRendererThree extends WorldRendererCommon {
for (const onRender of this.onRender) { for (const onRender of this.onRender) {
onRender() onRender()
} }
const end = performance.now()
const totalTime = end - start
this.renderTimeAvgCount++
this.renderTimeAvg = ((this.renderTimeAvg * (this.renderTimeAvgCount - 1)) + totalTime) / this.renderTimeAvgCount
this.renderTimeMax = Math.max(this.renderTimeMax, totalTime)
} }
renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) { renderHead (position: Vec3, rotation: number, isWall: boolean, blockEntity) {

View file

@ -51,6 +51,13 @@ export type AppQsParams = {
replayStopOnError?: string replayStopOnError?: string
replaySkipMissingOnTimeout?: string replaySkipMissingOnTimeout?: string
replayPacketsSenderDelay?: string replayPacketsSenderDelay?: string
// Benchmark params
openBenchmark?: string
renderDistance?: string
downloadBenchmark?: string
benchmarkMapZipUrl?: string
benchmarkPosition?: string
} }
export type AppQsParamsArray = { export type AppQsParamsArray = {

179
src/benchmark.ts Normal file
View file

@ -0,0 +1,179 @@
import { Vec3 } from 'vec3'
import { WorldRendererCommon } from 'renderer/viewer/lib/worldrendererCommon'
import prettyBytes from 'pretty-bytes'
import { subscribe } from 'valtio'
import { downloadAndOpenMapFromUrl } from './downloadAndOpenFile'
import { activeModalStack, miscUiState } from './globalState'
import { disabledSettings, options } from './optionsStorage'
import { BenchmarkAdapterInfo, getAllInfoLines } from './benchmarkAdapter'
import { appQueryParams } from './appParams'
const DEFAULT_RENDER_DISTANCE = 8
const fixtures = {
default: {
url: 'https://bucket.mcraft.fun/Future CITY 4.4-slim.zip',
spawn: [-133, 87, 309] as [number, number, number],
},
}
Error.stackTraceLimit = Error.stackTraceLimit < 30 ? 30 : Error.stackTraceLimit
export const openBenchmark = async (renderDistance = DEFAULT_RENDER_DISTANCE) => {
const fixture: {
url: string
spawn?: [number, number, number]
} = appQueryParams.benchmarkMapZipUrl ? {
url: appQueryParams.benchmarkMapZipUrl,
spawn: appQueryParams.benchmarkPosition ? appQueryParams.benchmarkPosition.split(',').map(Number) as [number, number, number] : fixtures.default.spawn,
} : fixtures.default
let memoryUsageAverage = 0
let memoryUsageSamples = 0
let memoryUsageWorst = 0
setInterval(() => {
const memoryUsage = (window.performance as any)?.memory?.usedJSHeapSize
if (memoryUsage) {
memoryUsageAverage = (memoryUsageAverage * memoryUsageSamples + memoryUsage) / (memoryUsageSamples + 1)
memoryUsageSamples++
if (memoryUsage > memoryUsageWorst) {
memoryUsageWorst = memoryUsage
}
}
}, 200)
let benchmarkName = `${fixture.url}`
if (fixture.spawn) {
benchmarkName += ` - ${fixture.spawn.join(',')}`
}
benchmarkName += ` - ${renderDistance}`
const benchmarkAdapter: BenchmarkAdapterInfo = {
get benchmarkName () {
return benchmarkName
},
get worldLoadTimeSeconds () {
return window.worldLoadTime
},
get mesherWorkersCount () {
return (window.world as WorldRendererCommon).worldRendererConfig.mesherWorkers
},
get mesherProcessAvgMs () {
return (window.world as WorldRendererCommon).workersProcessAverageTime
},
get mesherProcessWorstMs () {
return (window.world as WorldRendererCommon).maxWorkersProcessTime
},
get averageRenderTimeMs () {
return (window.world as WorldRendererCommon).renderTimeAvg
},
get worstRenderTimeMs () {
return (window.world as WorldRendererCommon).renderTimeMax
},
get fpsAveragePrediction () {
const avgRenderTime = (window.world as WorldRendererCommon).renderTimeAvg
return 1000 / avgRenderTime
},
get fpsWorstPrediction () {
const maxRenderTime = (window.world as WorldRendererCommon).renderTimeMax
return 1000 / maxRenderTime
},
get fpsAverageReal () {
return -1
},
get fpsWorstReal () {
return -1
},
get memoryUsageAverage () {
return prettyBytes(memoryUsageAverage)
},
get memoryUsageWorst () {
return prettyBytes(memoryUsageWorst)
},
get gpuInfo () {
return appViewer.rendererState.renderer
},
get hardwareConcurrency () {
return navigator.hardwareConcurrency
},
get userAgent () {
return navigator.userAgent
},
}
window.benchmarkAdapter = benchmarkAdapter
disabledSettings.value.add('renderDistance')
options.renderDistance = renderDistance
void downloadAndOpenMapFromUrl(fixture.url, undefined, {
connectEvents: {
serverCreated () {
if (fixture.spawn) {
localServer!.spawnPoint = new Vec3(...fixture.spawn)
localServer!.on('newPlayer', (player) => {
player.on('dataLoaded', () => {
player.position = new Vec3(...fixture.spawn!)
})
})
}
},
}
})
document.addEventListener('cypress-world-ready', () => {
let stats = getAllInfoLines(window.benchmarkAdapter)
if (appQueryParams.downloadBenchmark) {
const a = document.createElement('a')
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(stats.join('\n'))
a.download = `benchmark-${appViewer.backend?.id}.txt`
a.click()
}
const panel = document.createElement('div')
panel.style.position = 'fixed'
panel.style.top = '10px'
panel.style.right = '10px'
panel.style.backgroundColor = 'rgba(0,0,0,0.8)'
panel.style.color = 'white'
panel.style.padding = '10px'
panel.style.zIndex = '1000'
panel.style.fontFamily = 'monospace'
panel.id = 'benchmark-panel'
const pre = document.createElement('pre')
panel.appendChild(pre)
pre.textContent = stats.join('\n')
const updateStats = () => {
stats = getAllInfoLines(window.benchmarkAdapter)
pre.textContent = stats.join('\n')
}
document.body.appendChild(panel)
// setInterval(updateStats, 100)
})
}
document.addEventListener('pointerlockchange', (e) => {
const panel = document.querySelector<HTMLDivElement>('#benchmark-panel')
if (panel) {
panel.hidden = !!document.pointerLockElement
}
})
subscribe(activeModalStack, () => {
const panel = document.querySelector<HTMLDivElement>('#benchmark-panel')
if (panel && activeModalStack.length > 1) {
panel.hidden = true
}
})
export const registerOpenBenchmarkListener = () => {
if (appQueryParams.openBenchmark) {
void openBenchmark(appQueryParams.renderDistance ? +appQueryParams.renderDistance : undefined)
}
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyB' && e.shiftKey && !miscUiState.gameLoaded && activeModalStack.length === 0) {
e.preventDefault()
void openBenchmark()
}
})
}

58
src/benchmarkAdapter.ts Normal file
View file

@ -0,0 +1,58 @@
import { noCase } from 'change-case'
export interface BenchmarkAdapterInfo {
benchmarkName: string
// general load info
worldLoadTimeSeconds: number
// mesher
mesherWorkersCount: number
mesherProcessAvgMs: number
mesherProcessWorstMs: number
// rendering backend
averageRenderTimeMs: number
worstRenderTimeMs: number
fpsAveragePrediction: number
fpsWorstPrediction: number
fpsAverageReal: number
fpsWorstReal: number
// memory total
memoryUsageAverage: string
memoryUsageWorst: string
// context info
gpuInfo: string
hardwareConcurrency: number
userAgent: string
}
export const getAllInfo = (adapter: BenchmarkAdapterInfo) => {
return Object.fromEntries(
Object.entries(adapter).map(([key, value]) => {
if (typeof value === 'function') {
value = (value as () => any)()
}
if (typeof value === 'number') {
value = value.toFixed(2)
}
return [noCase(key), value]
})
)
}
export const getAllInfoLines = (adapter: BenchmarkAdapterInfo, delayed = false) => {
const info = getAllInfo(adapter)
if (delayed) {
for (const key in info) {
if (key !== 'fpsAveragePrediction' && key !== 'fpsAverageReal') {
delete info[key]
}
}
}
return Object.entries(info).map(([key, value]) => {
return `${key}${delayed ? ' (delayed)' : ''}: ${value}`
})
}

View file

@ -16,6 +16,7 @@ import { packetsReplayState } from './react/state/packetsReplayState'
import { createFullScreenProgressReporter } from './core/progressReporter' import { createFullScreenProgressReporter } from './core/progressReporter'
import { showNotification } from './react/NotificationProvider' import { showNotification } from './react/NotificationProvider'
import { resetAppStorage } from './react/appStorageProvider' import { resetAppStorage } from './react/appStorageProvider'
import { ConnectOptions } from './connect'
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
browserfs.install(window) browserfs.install(window)
@ -558,7 +559,7 @@ export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | und
} }
// todo rename method // todo rename method
const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) => { const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'], connectOptions?: Partial<ConnectOptions>) => {
await new Promise<void>(async resolve => { await new Promise<void>(async resolve => {
browserfs.configure({ browserfs.configure({
// todo // todo
@ -603,7 +604,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'])
} }
if (availableWorlds.length === 1) { if (availableWorlds.length === 1) {
await loadSave(`/world/${availableWorlds[0]}`) await loadSave(`/world/${availableWorlds[0]}`, connectOptions)
return return
} }

View file

@ -15,7 +15,7 @@ export type ConnectOptions = {
singleplayer?: any singleplayer?: any
username: string username: string
proxy?: string proxy?: string
botVersion?: any botVersion?: string
serverOverrides? serverOverrides?
serverOverridesFlat? serverOverridesFlat?
peerId?: string peerId?: string
@ -23,7 +23,6 @@ export type ConnectOptions = {
onSuccessfulPlay?: () => void onSuccessfulPlay?: () => void
autoLoginPassword?: string autoLoginPassword?: string
serverIndex?: string serverIndex?: string
/** If true, will show a UI to authenticate with a new account */
authenticatedAccount?: AuthenticatedAccount | true authenticatedAccount?: AuthenticatedAccount | true
peerOptions?: any peerOptions?: any
viewerWsConnect?: string viewerWsConnect?: string
@ -31,6 +30,15 @@ export type ConnectOptions = {
/** Will enable local replay server */ /** Will enable local replay server */
worldStateFileContents?: string worldStateFileContents?: string
connectEvents?: {
serverCreated?: () => void
// connect: () => void;
// disconnect: () => void;
// error: (err: any) => void;
// ready: () => void;
// end: () => void;
}
} }
export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => { export const getVersionAutoSelect = (autoVersionSelect = options.serversAutoVersionSelect) => {

View file

@ -5,6 +5,7 @@ import { setLoadingScreenStatus } from './appStatus'
import { appQueryParams, appQueryParamsArray } from './appParams' import { appQueryParams, appQueryParamsArray } from './appParams'
import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets' import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets'
import { createFullScreenProgressReporter } from './core/progressReporter' import { createFullScreenProgressReporter } from './core/progressReporter'
import { ConnectOptions } from './connect'
export const getFixedFilesize = (bytes: number) => { export const getFixedFilesize = (bytes: number) => {
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
@ -65,24 +66,29 @@ const inner = async () => {
await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined) await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined)
return true return true
} }
if (mapUrlDirGuess) { if (mapUrlDirGuess) {
// await openWorldFromHttpDir(undefined, mapUrlDirGuess) // await openWorldFromHttpDir(undefined, mapUrlDirGuess)
return true return true
} }
let mapUrl = appQueryParams.map
const { texturepack } = appQueryParams const { map, texturepack } = appQueryParams
return downloadAndOpenMapFromUrl(map, texturepack)
}
export const downloadAndOpenMapFromUrl = async (mapUrl: string | undefined, texturepackUrl: string | undefined, connectOptions?: Partial<ConnectOptions>) => {
// fixme // fixme
if (texturepack) mapUrl = texturepack if (texturepackUrl) mapUrl = texturepackUrl
if (!mapUrl) return false if (!mapUrl) return false
if (texturepack) { if (texturepackUrl) {
await updateTexturePackInstalledState() await updateTexturePackInstalledState()
if (resourcePackState.resourcePackInstalled) { if (resourcePackState.resourcePackInstalled) {
if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackNames()[0]} Continue?`)) return if (!confirm(`You are going to install a new resource pack, which will REPLACE the current one: ${await getResourcePackNames()[0]} Continue?`)) return
} }
} }
const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25) const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-25)
const downloadThing = texturepack ? 'texturepack' : 'world' const downloadThing = texturepackUrl ? 'texturepack' : 'world'
setLoadingScreenStatus(`Downloading ${downloadThing} ${name}...`) setLoadingScreenStatus(`Downloading ${downloadThing} ${name}...`)
const response = await fetch(mapUrl) const response = await fetch(mapUrl)
@ -115,25 +121,25 @@ const inner = async () => {
const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined
setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true) setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true)
// Pass the received data to the controller // Pass the received data to the controller
controller.enqueue(value) controller.enqueue(value)
} }
}, },
})).arrayBuffer() })).arrayBuffer()
if (texturepack) { if (texturepackUrl) {
const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30) const name = mapUrl.slice(mapUrl.lastIndexOf('/') + 1).slice(-30)
await installResourcepackPack(buffer, createFullScreenProgressReporter(), name) await installResourcepackPack(buffer, createFullScreenProgressReporter(), name)
} else { } else {
await openWorldZip(buffer) await openWorldZip(buffer, undefined, connectOptions)
} }
return true
} }
export default async () => { export default async () => {
try { try {
return await inner() return await inner()
} catch (err) { } catch (err) {
setLoadingScreenStatus(`Failed to download. Either refresh page or remove map param from URL. Reason: ${err.message}`) setLoadingScreenStatus(`Failed to download/open. Either refresh page or remove map param from URL. Reason: ${err.message}`)
return true return true
} }
} }

View file

@ -96,6 +96,7 @@ import { localRelayServerPlugin } from './mineflayer/plugins/packetsRecording'
import { createConsoleLogProgressReporter, createFullScreenProgressReporter, ProgressReporter } from './core/progressReporter' import { createConsoleLogProgressReporter, createFullScreenProgressReporter, ProgressReporter } from './core/progressReporter'
import { appViewer } from './appViewer' import { appViewer } from './appViewer'
import './appViewerLoad' import './appViewerLoad'
import { registerOpenBenchmarkListener } from './benchmark'
window.debug = debug window.debug = debug
window.beforeRenderFrame = [] window.beforeRenderFrame = []
@ -119,13 +120,22 @@ function hideCurrentScreens () {
insertActiveModalStack('', []) insertActiveModalStack('', [])
} }
const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => { const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}, connectOptions?: Partial<ConnectOptions>) => {
const serverSettingsQsRaw = appQueryParamsArray.serverSetting ?? [] const serverSettingsQsRaw = appQueryParamsArray.serverSetting ?? []
const serverSettingsQs = serverSettingsQsRaw.map(x => x.split(':')).reduce<Record<string, string>>((acc, [key, value]) => { const serverSettingsQs = serverSettingsQsRaw.map(x => x.split(':')).reduce<Record<string, string>>((acc, [key, value]) => {
acc[key] = JSON.parse(value) acc[key] = JSON.parse(value)
return acc return acc
}, {}) }, {})
void connect({ singleplayer: true, username: options.localUsername, serverOverrides, serverOverridesFlat: { ...flattenedServerOverrides, ...serverSettingsQs } }) void connect({
singleplayer: true,
username: options.localUsername,
serverOverrides,
serverOverridesFlat: {
...flattenedServerOverrides,
...serverSettingsQs
},
...connectOptions
})
} }
function listenGlobalEvents () { function listenGlobalEvents () {
window.addEventListener('connect', e => { window.addEventListener('connect', e => {
@ -133,7 +143,9 @@ function listenGlobalEvents () {
void connect(options) void connect(options)
}) })
window.addEventListener('singleplayer', (e) => { window.addEventListener('singleplayer', (e) => {
loadSingleplayer((e as CustomEvent).detail) const { detail } = (e as CustomEvent)
const { connectOptions, ...rest } = detail
loadSingleplayer(rest, {}, connectOptions)
}) })
} }
@ -348,6 +360,7 @@ export async function connect (connectOptions: ConnectOptions) {
// 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 // 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
localServer = window.localServer = window.server = startLocalServer(serverOptions) localServer = window.localServer = window.server = startLocalServer(serverOptions)
connectOptions?.connectEvents?.serverCreated?.()
// todo need just to call quit if started // todo need just to call quit if started
// loadingScreen.maybeRecoverable = false // loadingScreen.maybeRecoverable = false
// init world, todo: do it for any async plugins // init world, todo: do it for any async plugins
@ -644,13 +657,15 @@ export async function connect (connectOptions: ConnectOptions) {
setLoadingScreenStatus('Loading world') setLoadingScreenStatus('Loading world')
}) })
const loadStart = Date.now()
let worldWasReady = false let worldWasReady = false
const waitForChunksToLoad = async (progress?: ProgressReporter) => { const waitForChunksToLoad = async (progress?: ProgressReporter) => {
await new Promise<void>(resolve => { await new Promise<void>(resolve => {
if (worldWasReady) {
resolve()
return
}
const unsub = subscribe(appViewer.rendererState, () => { const unsub = subscribe(appViewer.rendererState, () => {
if (worldWasReady) return if (appViewer.rendererState.world.allChunksLoaded && appViewer.nonReactiveState.world.chunksTotalNumber) {
if (appViewer.rendererState.world.allChunksLoaded) {
worldWasReady = true worldWasReady = true
resolve() resolve()
unsub() unsub()
@ -662,11 +677,6 @@ export async function connect (connectOptions: ConnectOptions) {
}) })
} }
void waitForChunksToLoad().then(() => {
console.log('All chunks done and ready! Time from renderer connect to ready', (Date.now() - loadStart) / 1000, 's')
document.dispatchEvent(new Event('cypress-world-ready'))
})
const spawnEarlier = !singleplayer && !p2pMultiplayer const spawnEarlier = !singleplayer && !p2pMultiplayer
const displayWorld = async () => { const displayWorld = async () => {
if (resourcePackState.isServerInstalling) { if (resourcePackState.isServerInstalling) {
@ -679,11 +689,18 @@ export async function connect (connectOptions: ConnectOptions) {
}) })
await appViewer.resourcesManager.promiseAssetsReady await appViewer.resourcesManager.promiseAssetsReady
} }
console.log('try to focus window')
window.focus?.()
errorAbortController.abort() errorAbortController.abort()
if (appStatusState.isError) return if (appStatusState.isError) return
const loadWorldStart = Date.now()
console.log('try to focus window')
window.focus?.()
void waitForChunksToLoad().then(() => {
window.worldLoadTime = (Date.now() - loadWorldStart) / 1000
console.log('All chunks done and ready! Time from renderer connect to ready', (Date.now() - loadWorldStart) / 1000, 's')
document.dispatchEvent(new Event('cypress-world-ready'))
})
try { try {
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout) if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
playerState.onlineMode = !!connectOptions.authenticatedAccount playerState.onlineMode = !!connectOptions.authenticatedAccount
@ -932,7 +949,7 @@ if (!reconnectOptions) {
} }
}, (err) => { }, (err) => {
console.error(err) console.error(err)
alert(`Failed to download file: ${err}`) alert(`Something went wrong: ${err}`)
}) })
} }
@ -945,3 +962,4 @@ if (initialLoader) {
window.pageLoaded = true window.pageLoaded = true
void possiblyHandleStateVariable() void possiblyHandleStateVariable()
registerOpenBenchmarkListener()

View file

@ -11,6 +11,7 @@ import { isMajorVersionGreater } from './utils'
import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState' import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState'
import supportedVersions from './supportedVersions.mjs' import supportedVersions from './supportedVersions.mjs'
import { ConnectOptions } from './connect'
import { appQueryParams } from './appParams' import { appQueryParams } from './appParams'
// todo include name of opened handle (zip)! // todo include name of opened handle (zip)!
@ -49,7 +50,7 @@ export const readLevelDat = async (path) => {
return { levelDat, dataRaw: parsed.value.Data!.value as Record<string, any> } return { levelDat, dataRaw: parsed.value.Data!.value as Record<string, any> }
} }
export const loadSave = async (root = '/world') => { export const loadSave = async (root = '/world', connectOptions?: Partial<ConnectOptions>) => {
// todo test // todo test
if (miscUiState.gameLoaded) { if (miscUiState.gameLoaded) {
await disconnect() await disconnect()
@ -194,7 +195,8 @@ export const loadSave = async (root = '/world') => {
} : {}, } : {},
...root === '/world' ? {} : { ...root === '/world' ? {} : {
'worldFolder': root 'worldFolder': root
} },
connectOptions
}, },
})) }))
} }

View file

@ -1,9 +1,71 @@
import { subscribeKey } from 'valtio/utils'
import { preventThrottlingWithSound } from '../core/timers' import { preventThrottlingWithSound } from '../core/timers'
import { options } from '../optionsStorage' import { options } from '../optionsStorage'
customEvents.on('mineflayerBotCreated', () => { customEvents.on('mineflayerBotCreated', () => {
if (options.preventBackgroundTimeoutKick) { const abortController = new AbortController()
const unsub = preventThrottlingWithSound()
bot.on('end', unsub) const maybeGoBackgroundKickPrevention = () => {
if (options.preventBackgroundTimeoutKick && !bot.backgroundKickPrevention) {
const unsub = preventThrottlingWithSound()
bot.on('end', unsub)
bot.backgroundKickPrevention = true
}
} }
maybeGoBackgroundKickPrevention()
subscribeKey(options, 'preventBackgroundTimeoutKick', (value) => {
maybeGoBackgroundKickPrevention()
})
// wake lock
const requestWakeLock = async () => {
if (!('wakeLock' in navigator)) {
console.warn('Wake Lock API is not supported in this browser')
return
}
if (options.preventSleep && !bot.wakeLock && !bot.lockRequested) {
bot.lockRequested = true
bot.wakeLock = await navigator.wakeLock.request('screen').finally(() => {
bot.lockRequested = false
})
bot.wakeLock.addEventListener('release', () => {
bot.wakeLock = undefined
}, {
once: true,
})
}
if (!options.preventSleep && bot.wakeLock) {
void bot.wakeLock.release()
}
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// we are back to the tab, request wake lock again
void requestWakeLock()
}
}, {
signal: abortController.signal,
})
void requestWakeLock()
subscribeKey(options, 'preventSleep', (value) => {
void requestWakeLock()
})
bot.on('end', () => {
if (bot.wakeLock) {
void bot.wakeLock.release()
}
abortController.abort()
})
}) })
declare module 'mineflayer' {
interface Bot {
backgroundKickPrevention?: boolean
wakeLock?: WakeLockSentinel
lockRequested?: boolean
}
}

View file

@ -526,7 +526,11 @@ export const guiOptionsScheme: {
}, },
}, },
{ {
preventBackgroundTimeoutKick: {} preventBackgroundTimeoutKick: {},
preventSleep: {
disabledReason: navigator.wakeLock ? undefined : 'Your browser does not support wake lock API',
enableWarning: 'When connected to a server, prevent PC from sleeping or screen dimming. Useful for purpusely staying AFK for long time. Some events might still prevent this like loosing tab focus or going low power mode.',
},
}, },
{ {
custom () { custom () {

View file

@ -65,6 +65,7 @@ const defaultOptions = {
waitForChunksRender: 'sp-only' as 'sp-only' | boolean, waitForChunksRender: 'sp-only' as 'sp-only' | boolean,
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>, jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
preventBackgroundTimeoutKick: false, preventBackgroundTimeoutKick: false,
preventSleep: false,
// antiAliasing: false, // antiAliasing: false,

View file

@ -46,7 +46,8 @@ export const defaultIndicatorsState = {
readonlyFiles: false, readonlyFiles: false,
writingFiles: false, // saving writingFiles: false, // saving
appHasErrors: false, appHasErrors: false,
connectionIssues: 0 connectionIssues: 0,
preventSleep: false,
} }
const indicatorIcons: Record<keyof typeof defaultIndicatorsState, string> = { const indicatorIcons: Record<keyof typeof defaultIndicatorsState, string> = {
@ -56,6 +57,7 @@ const indicatorIcons: Record<keyof typeof defaultIndicatorsState, string> = {
appHasErrors: 'alert', appHasErrors: 'alert',
readonlyFiles: 'file-off', readonlyFiles: 'file-off',
connectionIssues: pixelartIcons['cellular-signal-off'], connectionIssues: pixelartIcons['cellular-signal-off'],
preventSleep: pixelartIcons.moon,
} }
const colorOverrides = { const colorOverrides = {

View file

@ -1,5 +1,5 @@
import { proxy, subscribe, useSnapshot } from 'valtio' import { proxy, subscribe, useSnapshot } from 'valtio'
import { useEffect, useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { subscribeKey } from 'valtio/utils' import { subscribeKey } from 'valtio/utils'
import { inGameError } from '../utils' import { inGameError } from '../utils'
import { fsState } from '../loadSave' import { fsState } from '../loadSave'
@ -51,6 +51,7 @@ const getEffectIndex = (newEffect: EffectType) => {
} }
export default () => { export default () => {
const [dummyState, setDummyState] = useState(false)
const stateIndicators = useSnapshot(state.indicators) const stateIndicators = useSnapshot(state.indicators)
const chunksLoading = !useSnapshot(appViewer.rendererState).world.allChunksLoaded const chunksLoading = !useSnapshot(appViewer.rendererState).world.allChunksLoaded
const { mesherWork } = useSnapshot(appViewer.rendererState).world const { mesherWork } = useSnapshot(appViewer.rendererState).world
@ -66,12 +67,21 @@ export default () => {
appHasErrors: hasErrors, appHasErrors: hasErrors,
connectionIssues: poorConnection ? 1 : noConnection ? 2 : 0, connectionIssues: poorConnection ? 1 : noConnection ? 2 : 0,
chunksLoading, chunksLoading,
preventSleep: !!bot.wakeLock,
// mesherWork, // mesherWork,
...stateIndicators, ...stateIndicators,
} }
const effects = useSnapshot(state.effects) const effects = useSnapshot(state.effects)
useEffect(() => {
// update bot related states
const interval = setInterval(() => {
setDummyState(s => !s)
}, 1000)
return () => clearInterval(interval)
}, [])
useMemo(() => { useMemo(() => {
const effectsImages = Object.fromEntries(loadedData.effectsArray.map((effect) => { const effectsImages = Object.fromEntries(loadedData.effectsArray.map((effect) => {
const nameKebab = effect.name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1) const nameKebab = effect.name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1)