Release (#248)
This commit is contained in:
commit
17df58de1d
40 changed files with 1651 additions and 2046 deletions
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -40,3 +40,25 @@ jobs:
|
|||
# if: ${{ github.event.pull_request.base.ref == 'release' }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
dedupe-check:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.ref == 'next'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9.0.4
|
||||
|
||||
- name: Run pnpm dedupe
|
||||
run: pnpm dedupe
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if ! git diff --exit-code --quiet pnpm-lock.yaml; then
|
||||
echo "pnpm dedupe introduced changes:"
|
||||
git diff --color=always pnpm-lock.yaml
|
||||
exit 1
|
||||
else
|
||||
echo "No changes detected after pnpm dedupe in pnpm-lock.yaml"
|
||||
fi
|
||||
23
.github/workflows/next-deploy.yml
vendored
23
.github/workflows/next-deploy.yml
vendored
|
|
@ -43,3 +43,26 @@ jobs:
|
|||
for alias in $(echo ${{ secrets.TEST_PREVIEW_DOMAIN }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:next`,
|
||||
base: 'release',
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (pulls.length === 0) {
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Release',
|
||||
head: 'next',
|
||||
base: 'release',
|
||||
body: 'PR was created automatically by the release workflow, hope you release it as soon as possible!',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -18,5 +18,6 @@ out
|
|||
generated
|
||||
storybook-static
|
||||
server-jar
|
||||
config.local.json
|
||||
|
||||
src/react/npmReactComponents.ts
|
||||
|
|
|
|||
|
|
@ -31,4 +31,5 @@ RUN npm i -g pnpm@9.0.4
|
|||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
VOLUME /app/dist
|
||||
ENTRYPOINT ["node", "server.js", "--prod"]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"prod-start": "node server.js --prod",
|
||||
"test-mc-server": "tsx cypress/minecraft-server.mjs",
|
||||
"lint": "eslint \"{src,cypress,prismarine-viewer}/**/*.{ts,js,jsx,tsx}\"",
|
||||
"lint-fix": "pnpm lint --fix",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
||||
|
|
@ -73,7 +74,7 @@
|
|||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minecraft-data": "3.80.0",
|
||||
"minecraft-data": "3.83.1",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
|
|
@ -142,7 +143,7 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.26",
|
||||
"mc-assets": "^0.2.28",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
@ -174,7 +175,7 @@
|
|||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.80.0",
|
||||
"minecraft-data": "3.83.1",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
|
|
@ -189,7 +190,7 @@
|
|||
"three@0.154.0": "patches/three@0.154.0.patch",
|
||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||
"minecraft-protocol@1.51.0": "patches/minecraft-protocol@1.49.0.patch"
|
||||
"minecraft-protocol@1.54.0": "patches/minecraft-protocol@1.54.0.patch"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.0.4"
|
||||
|
|
|
|||
|
|
@ -95,10 +95,10 @@ index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108
|
|||
|
||||
function onJoinServerResponse (err) {
|
||||
diff --git a/src/client.js b/src/client.js
|
||||
index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c278950ba7f396 100644
|
||||
index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644
|
||||
--- a/src/client.js
|
||||
+++ b/src/client.js
|
||||
@@ -88,10 +88,12 @@ class Client extends EventEmitter {
|
||||
@@ -89,10 +89,12 @@ class Client extends EventEmitter {
|
||||
parsed.metadata.name = parsed.data.name
|
||||
parsed.data = parsed.data.params
|
||||
parsed.metadata.state = state
|
||||
|
|
@ -115,7 +115,7 @@ index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c27895
|
|||
}
|
||||
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
||||
if (this._mcBundle.length) { // End bundle
|
||||
@@ -109,7 +111,13 @@ class Client extends EventEmitter {
|
||||
@@ -110,7 +112,13 @@ class Client extends EventEmitter {
|
||||
this._hasBundlePacket = false
|
||||
}
|
||||
} else {
|
||||
|
|
@ -130,7 +130,7 @@ index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c27895
|
|||
}
|
||||
})
|
||||
}
|
||||
@@ -167,7 +175,10 @@ class Client extends EventEmitter {
|
||||
@@ -168,7 +176,10 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
const onFatalError = (err) => {
|
||||
|
|
@ -142,7 +142,7 @@ index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c27895
|
|||
endSocket()
|
||||
}
|
||||
|
||||
@@ -196,6 +207,8 @@ class Client extends EventEmitter {
|
||||
@@ -197,6 +208,8 @@ class Client extends EventEmitter {
|
||||
serializer -> framer -> socket -> splitter -> deserializer */
|
||||
if (this.serializer) {
|
||||
this.serializer.end()
|
||||
|
|
@ -151,7 +151,7 @@ index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c27895
|
|||
} else {
|
||||
if (this.socket) this.socket.end()
|
||||
}
|
||||
@@ -237,8 +250,11 @@ class Client extends EventEmitter {
|
||||
@@ -238,8 +251,11 @@ class Client extends EventEmitter {
|
||||
|
||||
write (name, params) {
|
||||
if (!this.serializer.writable) { return }
|
||||
|
|
@ -166,10 +166,10 @@ index 5b63c295080f62ca54928660cdfa134214002fa1..d2d24bff963d8ed7747ccb48f8c27895
|
|||
}
|
||||
|
||||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index 423085259176a10c9dfeb617f00975df077d02be..ae0558b7c53ab24474b22240e8a2f4e4cde02f19 100644
|
||||
index e61d5403bab46251d35b22a2ea30eb09b2746a26..84f597427893671eeac231b11e6e42aa815601df 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -134,6 +134,7 @@ declare module 'minecraft-protocol' {
|
||||
@@ -135,6 +135,7 @@ declare module 'minecraft-protocol' {
|
||||
sessionServer?: string
|
||||
keepAlive?: boolean
|
||||
closeTimeout?: number
|
||||
|
|
@ -177,7 +177,7 @@ index 423085259176a10c9dfeb617f00975df077d02be..ae0558b7c53ab24474b22240e8a2f4e4
|
|||
noPongTimeout?: number
|
||||
checkTimeoutInterval?: number
|
||||
version?: string
|
||||
@@ -154,6 +155,8 @@ declare module 'minecraft-protocol' {
|
||||
@@ -155,6 +156,8 @@ declare module 'minecraft-protocol' {
|
||||
disableChatSigning?: boolean
|
||||
/** Pass custom client implementation if needed. */
|
||||
Client?: Client
|
||||
188
patches/minecraft-protocol@1.54.0.patch
Normal file
188
patches/minecraft-protocol@1.54.0.patch
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js
|
||||
index 3fe1552672e4c0dd1b14b3b56950c3d7eaf3537b..6eb615e5827279c328d5547b5911626693252da4 100644
|
||||
--- a/src/client/autoVersion.js
|
||||
+++ b/src/client/autoVersion.js
|
||||
@@ -9,7 +9,7 @@ module.exports = function (client, options) {
|
||||
client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed'
|
||||
debug('pinging', options.host)
|
||||
// TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping
|
||||
- ping(options, function (err, response) {
|
||||
+ ping(options, async function (err, response) {
|
||||
if (err) { return client.emit('error', err) }
|
||||
debug('ping response', response)
|
||||
// TODO: could also use ping pre-connect to save description, type, max players, etc.
|
||||
@@ -40,6 +40,7 @@ module.exports = function (client, options) {
|
||||
|
||||
// Reinitialize client object with new version TODO: move out of its constructor?
|
||||
client.version = minecraftVersion
|
||||
+ await options.versionSelectedHook?.(client)
|
||||
client.state = states.HANDSHAKING
|
||||
|
||||
// Let other plugins such as Forge/FML (modinfo) respond to the ping response
|
||||
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||
index f14269bea055d4329cd729271e7406ec4b344de7..00f5482eb6e3c911381ca9a728b1b4aae0d1d337 100644
|
||||
--- a/src/client/chat.js
|
||||
+++ b/src/client/chat.js
|
||||
@@ -111,7 +111,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (!player.chatSession) continue
|
||||
client._players[player.UUID] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||
sessionUuid: player.chatSession.uuid
|
||||
}
|
||||
@@ -127,7 +127,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (player.crypto) {
|
||||
client._players[player.UUID] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.crypto.publicKey,
|
||||
signature: player.crypto.signature,
|
||||
displayName: player.displayName || player.name
|
||||
@@ -198,7 +198,7 @@ module.exports = function (client, options) {
|
||||
if (mcData.supportFeature('useChatSessions')) {
|
||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||
- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
if (verified) client._signatureCache.push(packet.signature)
|
||||
client.emit('playerChat', {
|
||||
plainMessage: packet.plainMessage,
|
||||
@@ -363,7 +363,7 @@ module.exports = function (client, options) {
|
||||
}
|
||||
}
|
||||
|
||||
- client._signedChat = (message, options = {}) => {
|
||||
+ client._signedChat = async (message, options = {}) => {
|
||||
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||
options.salt = options.salt || 1n
|
||||
|
||||
@@ -405,7 +405,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
offset: client._lastSeenMessages.pending,
|
||||
acknowledged
|
||||
})
|
||||
@@ -419,7 +419,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
signedPreview: options.didPreview,
|
||||
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||
messageSender: e.sender,
|
||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||
index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
|
||||
--- a/src/client/encrypt.js
|
||||
+++ b/src/client/encrypt.js
|
||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||
if (packet.serverId !== '-') {
|
||||
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail')
|
||||
}
|
||||
- sendEncryptionKeyResponse()
|
||||
+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.')
|
||||
+ // sendEncryptionKeyResponse()
|
||||
+ // client.once('set_compression', () => {
|
||||
+ // clearTimeout(loginTimeout)
|
||||
+ // })
|
||||
}
|
||||
|
||||
function onJoinServerResponse (err) {
|
||||
diff --git a/src/client.js b/src/client.js
|
||||
index 74749698f8cee05b5dc749c271544f78d06645b0..e77e0a3f41c1ee780c3abbd54b0801d248c2a07c 100644
|
||||
--- a/src/client.js
|
||||
+++ b/src/client.js
|
||||
@@ -89,10 +89,12 @@ class Client extends EventEmitter {
|
||||
parsed.metadata.name = parsed.data.name
|
||||
parsed.data = parsed.data.params
|
||||
parsed.metadata.state = state
|
||||
- debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
- if (debug.enabled) {
|
||||
- const s = JSON.stringify(parsed.data, null, 2)
|
||||
- debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
|
||||
+ debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
+ if (debug.enabled) {
|
||||
+ const s = JSON.stringify(parsed.data, null, 2)
|
||||
+ debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ }
|
||||
}
|
||||
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
||||
if (this._mcBundle.length) { // End bundle
|
||||
@@ -110,7 +112,13 @@ class Client extends EventEmitter {
|
||||
this._hasBundlePacket = false
|
||||
}
|
||||
} else {
|
||||
- emitPacket(parsed)
|
||||
+ try {
|
||||
+ emitPacket(parsed)
|
||||
+ } catch (err) {
|
||||
+ console.log('Client incorrectly handled packet ' + parsed.metadata.name)
|
||||
+ console.error(err)
|
||||
+ // todo investigate why it doesn't close the stream even if unhandled there
|
||||
+ }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -168,7 +176,10 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
const onFatalError = (err) => {
|
||||
- this.emit('error', err)
|
||||
+ // todo find out what is trying to write after client disconnect
|
||||
+ if(err.code !== 'ECONNABORTED') {
|
||||
+ this.emit('error', err)
|
||||
+ }
|
||||
endSocket()
|
||||
}
|
||||
|
||||
@@ -197,6 +208,8 @@ class Client extends EventEmitter {
|
||||
serializer -> framer -> socket -> splitter -> deserializer */
|
||||
if (this.serializer) {
|
||||
this.serializer.end()
|
||||
+ this.socket?.end()
|
||||
+ this.socket?.emit('end')
|
||||
} else {
|
||||
if (this.socket) this.socket.end()
|
||||
}
|
||||
@@ -238,8 +251,11 @@ class Client extends EventEmitter {
|
||||
|
||||
write (name, params) {
|
||||
if (!this.serializer.writable) { return }
|
||||
- debug('writing packet ' + this.state + '.' + name)
|
||||
- debug(params)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
|
||||
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
|
||||
+ debug(params)
|
||||
+ }
|
||||
+ this.emit('writePacket', name, params)
|
||||
this.serializer.write({ name, params })
|
||||
}
|
||||
|
||||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index e61d5403bab46251d35b22a2ea30eb09b2746a26..84f597427893671eeac231b11e6e42aa815601df 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -135,6 +135,7 @@ declare module 'minecraft-protocol' {
|
||||
sessionServer?: string
|
||||
keepAlive?: boolean
|
||||
closeTimeout?: number
|
||||
+ closeTimeout?: number
|
||||
noPongTimeout?: number
|
||||
checkTimeoutInterval?: number
|
||||
version?: string
|
||||
@@ -155,6 +156,8 @@ declare module 'minecraft-protocol' {
|
||||
disableChatSigning?: boolean
|
||||
/** Pass custom client implementation if needed. */
|
||||
Client?: Client
|
||||
+ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */
|
||||
+ versionSelectedHook?: (client: Client) => Promise<void> | void
|
||||
}
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
2775
pnpm-lock.yaml
generated
2775
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -295,7 +295,7 @@ class MainScene extends BasePlaygroundScene {
|
|||
}
|
||||
}
|
||||
|
||||
worldView!.setBlockStateId(this.targetPos, block.stateId!)
|
||||
worldView!.setBlockStateId(this.targetPos, block.stateId)
|
||||
console.log('up stateId', block.stateId)
|
||||
this.params.metadata = block.metadata
|
||||
this.metadataGui.updateDisplay()
|
||||
|
|
|
|||
|
|
@ -276,8 +276,20 @@ export class Entities extends EventEmitter {
|
|||
// fixme workaround
|
||||
defaultSteveTexture
|
||||
|
||||
usernamePerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
|
||||
|
||||
// true means use default skin url
|
||||
updatePlayerSkin (entityId: string | number, username: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||
if (username) {
|
||||
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.usernamePerSkinUrlsCache[username] = {}
|
||||
if (typeof skinUrl === 'string') this.usernamePerSkinUrlsCache[username].skinUrl = skinUrl
|
||||
if (typeof capeUrl === 'string') this.usernamePerSkinUrlsCache[username].capeUrl = capeUrl
|
||||
if (skinUrl === true) {
|
||||
skinUrl = this.usernamePerSkinUrlsCache[username]?.skinUrl ?? skinUrl
|
||||
}
|
||||
capeUrl ??= this.usernamePerSkinUrlsCache[username]?.capeUrl
|
||||
}
|
||||
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
// const username = this.entities[entityId].username
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const addPositions = [
|
|||
|
||||
test('Known blocks are not rendered', () => {
|
||||
const { mesherWorld, getGeometry, pos, mcData } = setup(lastVersion, addPositions as any)
|
||||
const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava', 'moving_piston', 'light'])
|
||||
const ignoreAsExpected = new Set([...INVISIBLE_BLOCKS, 'water', 'lava'])
|
||||
|
||||
let time = 0
|
||||
let times = 0
|
||||
|
|
@ -49,8 +49,6 @@ test('Known blocks are not rendered', () => {
|
|||
// TODO resolve creaking_heart issue (1.21.3)
|
||||
expect(missingBlocks).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bubble_column": true,
|
||||
"creaking_heart": true,
|
||||
"end_gateway": true,
|
||||
"end_portal": true,
|
||||
"structure_void": true,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ import { INVISIBLE_BLOCKS } from './worldConstants'
|
|||
|
||||
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
||||
|
||||
const ALWAYS_WATERLOGGED = new Set([
|
||||
'seagrass',
|
||||
'tall_seagrass',
|
||||
'kelp',
|
||||
'kelp_plant',
|
||||
'bubble_column'
|
||||
])
|
||||
|
||||
function columnKey (x, z) {
|
||||
return `${x},${z}`
|
||||
}
|
||||
|
|
@ -160,6 +168,12 @@ export class World {
|
|||
if (block.models === undefined && blockProvider) {
|
||||
if (!attr) throw new Error('attr is required')
|
||||
const props = block.getProperties()
|
||||
|
||||
// Patch waterlogged property for ocean plants
|
||||
if (ALWAYS_WATERLOGGED.has(block.name)) {
|
||||
props.waterlogged = 'true'
|
||||
}
|
||||
|
||||
try {
|
||||
// fixme
|
||||
if (this.preflat) {
|
||||
|
|
@ -194,6 +208,7 @@ export class World {
|
|||
|
||||
if (block.name === 'flowing_water') block.name = 'water'
|
||||
if (block.name === 'flowing_lava') block.name = 'lava'
|
||||
if (block.name === 'bubble_column') block.name = 'water' // TODO need to distinguish between water and bubble column
|
||||
// block.position = loc // it overrides position of all currently loaded blocks
|
||||
block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0]
|
||||
if (block.name === 'redstone_ore') block.transparent = false
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier'])
|
||||
export const INVISIBLE_BLOCKS = new Set(['air', 'void_air', 'cave_air', 'barrier', 'light', 'moving_piston'])
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export class Viewer {
|
|||
}))
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.getMineflayerBot()?.entity?.eyeHeight ?? this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo need to cleanup
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
cameraRoll = 0
|
||||
|
||||
@worldCleanup()
|
||||
active = false
|
||||
|
|
@ -324,7 +325,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
Object.assign(blockTexturesChanges, christmasPack)
|
||||
}
|
||||
|
||||
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {}).filter(x => x.includes('/'))
|
||||
const customBlockTextures = Object.keys(this.customTextures.blocks?.textures ?? {})
|
||||
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
const texture = this.customTextures?.blocks?.textures[textureName]
|
||||
return blockTexturesChanges[textureName] ?? texture
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
if (pos) {
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
this.camera.rotation.set(pitch, yaw, this.cameraRoll, 'ZYX')
|
||||
}
|
||||
|
||||
render () {
|
||||
|
|
|
|||
|
|
@ -95,10 +95,14 @@ const appConfig = defineConfig({
|
|||
fs.copyFileSync('./assets/release.json', './dist/release.json')
|
||||
}
|
||||
const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
|
||||
let configLocalJson = {}
|
||||
try {
|
||||
configLocalJson = JSON.parse(fs.readFileSync('./config.local.json', 'utf8'))
|
||||
} catch (err) {}
|
||||
if (dev) {
|
||||
configJson.defaultProxy = ':8080'
|
||||
}
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify({ ...configJson, ...configLocalJson }), 'utf8')
|
||||
// childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ export const displayClientChat = (text: string) => {
|
|||
}
|
||||
|
||||
export const parseFormattedMessagePacket = (arg) => {
|
||||
// if (typeof arg === 'string') {
|
||||
// try {
|
||||
// arg = JSON.parse(arg)
|
||||
// return {
|
||||
// formatted: arg,
|
||||
// plain: ''
|
||||
// }
|
||||
// } catch {}
|
||||
// }
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -313,9 +313,11 @@ export function lockUrl () {
|
|||
newQs = `reconnect=1`
|
||||
} else if (lastConnectOptions.value?.server) {
|
||||
const qs = new URLSearchParams()
|
||||
const { server, botVersion } = lastConnectOptions.value
|
||||
qs.set('server', server)
|
||||
const { server, botVersion, proxy, username } = lastConnectOptions.value
|
||||
qs.set('ip', server)
|
||||
if (botVersion) qs.set('version', botVersion)
|
||||
if (proxy) qs.set('proxy', proxy)
|
||||
if (username) qs.set('username', username)
|
||||
newQs = String(qs.toString())
|
||||
}
|
||||
|
||||
|
|
@ -736,11 +738,22 @@ addEventListener('mousedown', async (e) => {
|
|||
window.addEventListener('keydown', (e) => {
|
||||
if (e.code !== 'Escape') return
|
||||
if (activeModalStack.length) {
|
||||
hideCurrentModal(undefined, () => {
|
||||
if (!activeModalStack.length) {
|
||||
pointerLock.justHitEscape = true
|
||||
const hideAll = e.ctrlKey || e.metaKey
|
||||
if (hideAll) {
|
||||
while (activeModalStack.length > 0) {
|
||||
hideCurrentModal(undefined, () => {
|
||||
if (!activeModalStack.length) {
|
||||
pointerLock.justHitEscape = true
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
hideCurrentModal(undefined, () => {
|
||||
if (!activeModalStack.length) {
|
||||
pointerLock.justHitEscape = true
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (pointerLock.hasPointerLock) {
|
||||
document.exitPointerLock?.()
|
||||
if (options.autoExitFullscreen) {
|
||||
|
|
|
|||
|
|
@ -153,4 +153,32 @@ customEvents.on('gameLoaded', () => {
|
|||
watchValue(options, o => {
|
||||
viewer.entities.setDebugMode(o.showChunkBorders ? 'basic' : 'none')
|
||||
})
|
||||
|
||||
// Texture override from packet properties
|
||||
bot._client.on('player_info', (packet) => {
|
||||
for (const player of packet.data) {
|
||||
const textureProperty = player.properties?.find(prop => prop.name === 'textures')
|
||||
if (textureProperty) {
|
||||
try {
|
||||
const textureData = JSON.parse(Buffer.from(textureProperty.value, 'base64').toString())
|
||||
const skinUrl = textureData.textures?.SKIN?.url
|
||||
const capeUrl = textureData.textures?.CAPE?.url
|
||||
|
||||
// Find entity with matching UUID and update skin
|
||||
let entityId = ''
|
||||
for (const [entId, entity] of Object.entries(bot.entities)) {
|
||||
if (entity.uuid === player.UUID) {
|
||||
entityId = entId
|
||||
break
|
||||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
viewer.entities.updatePlayerSkin(entityId, player.name, skinUrl, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,14 +6,7 @@ export interface ClientOnMap {
|
|||
} | /** 1.12.2 */ {
|
||||
keepAliveId: bigint;
|
||||
};
|
||||
login: /** 1.7 */ {
|
||||
entityId: number;
|
||||
gameMode: number;
|
||||
dimension: number;
|
||||
difficulty: number;
|
||||
maxPlayers: number;
|
||||
levelType: string;
|
||||
} | /** 1.8 */ {
|
||||
login:/** 1.8 */ {
|
||||
entityId: number;
|
||||
gameMode: number;
|
||||
dimension: number;
|
||||
|
|
@ -148,9 +141,7 @@ export interface ClientOnMap {
|
|||
entityId: number;
|
||||
equipments: any;
|
||||
};
|
||||
spawn_position: /** 1.7 */ {
|
||||
location: any;
|
||||
} | /** 1.8 */ {
|
||||
spawn_position:/** 1.8 */ {
|
||||
location: { x: number, y: number, z: number };
|
||||
} | /** 1.17 */ {
|
||||
location: { x: number, y: number, z: number };
|
||||
|
|
@ -215,14 +206,7 @@ export interface ClientOnMap {
|
|||
death: any;
|
||||
portalCooldown: number;
|
||||
};
|
||||
position: /** 1.7 */ {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
yaw: number;
|
||||
pitch: number;
|
||||
onGround: boolean;
|
||||
} | /** 1.8 */ {
|
||||
position: /** 1.8 */ {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
|
|
@ -905,11 +889,7 @@ export interface ClientOnMap {
|
|||
statistics: /** 1.7 */ {
|
||||
entries: any;
|
||||
};
|
||||
player_info: /** 1.7 */ {
|
||||
playerName: string;
|
||||
online: boolean;
|
||||
ping: number;
|
||||
} | /** 1.8 */ {
|
||||
player_info: /** 1.8 */ {
|
||||
action: number;
|
||||
data: any;
|
||||
};
|
||||
|
|
@ -926,22 +906,13 @@ export interface ClientOnMap {
|
|||
length: number;
|
||||
matches: any;
|
||||
};
|
||||
scoreboard_objective: /** 1.7 */ {
|
||||
name: string;
|
||||
displayText: string;
|
||||
action: number;
|
||||
} | /** 1.8 */ {
|
||||
scoreboard_objective:/** 1.8 */ {
|
||||
name: string;
|
||||
action: number;
|
||||
displayText: any;
|
||||
type: any;
|
||||
};
|
||||
scoreboard_score: /** 1.7 */ {
|
||||
itemName: string;
|
||||
action: number;
|
||||
scoreName: any;
|
||||
value: any;
|
||||
} | /** 1.8 */ {
|
||||
scoreboard_score:/** 1.8 */ {
|
||||
itemName: string;
|
||||
action: number;
|
||||
scoreName: string;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ export type AppConfig = {
|
|||
peerJsServerFallback?: string
|
||||
promoteServers?: Array<{ ip, description, version? }>
|
||||
mapsProvider?: string
|
||||
|
||||
defaultSettings?: Record<string, any>
|
||||
allowAutoConnect?: boolean
|
||||
}
|
||||
|
||||
export const miscUiState = proxy({
|
||||
|
|
|
|||
1
src/globals.d.ts
vendored
1
src/globals.d.ts
vendored
|
|
@ -26,6 +26,7 @@ declare const customEvents: import('typed-emitter').default<{
|
|||
mineflayerBotCreated (): void
|
||||
search (q: string): void
|
||||
activateItem (item: Item, slot: number, offhand: boolean): void
|
||||
hurtAnimation (): void
|
||||
}>
|
||||
declare const beforeRenderFrame: Array<() => void>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import './globals'
|
|||
import './devtools'
|
||||
import './entities'
|
||||
import './globalDomListeners'
|
||||
import './mineflayer/maps'
|
||||
import './mineflayer/cameraShake'
|
||||
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||
import { onGameLoad } from './inventoryWindows'
|
||||
import { supportedVersions } from 'minecraft-protocol'
|
||||
|
|
@ -650,6 +652,8 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
destroyAll()
|
||||
})
|
||||
|
||||
// bot.emit('kicked', '{"translate":"disconnect.genericReason","with":["Internal Exception: io.netty.handler.codec.EncoderException: com.viaversion.viaversion.exception.InformativeException: Please report this on the Via support Discord or open an issue on the relevant GitHub repository\\nPacket Type: SYSTEM_CHAT, Index: 1, Type: TagType, Data: [], Packet ID: 103, Source 0: com.viaversion.viabackwards.protocol.v1_20_3to1_20_2.Protocol1_20_3To1_20_2$$Lambda/0x00007f9930f63080"]}', false)
|
||||
|
||||
const packetBeforePlay = (_, __, ___, fullBuffer) => {
|
||||
lastPacket = fullBuffer.toString()
|
||||
}
|
||||
|
|
@ -1004,6 +1008,7 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
const ip = qs.get('ip')
|
||||
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
|
||||
void connect({
|
||||
botVersion: qs.get('version') ?? undefined,
|
||||
...lastConnect, // todo mixing is not good idea
|
||||
ip: ip || undefined
|
||||
})
|
||||
|
|
|
|||
|
|
@ -45,9 +45,13 @@ export const onGameLoad = (onLoad) => {
|
|||
if (!viewer.world.itemsAtlasParser) return
|
||||
itemsRenderer = new ItemsRenderer(bot.version, viewer.world.blockstatesModels, viewer.world.itemsAtlasParser, viewer.world.blocksAtlasParser)
|
||||
globalThis.itemsRenderer = itemsRenderer
|
||||
if (allImagesLoadedState.value) return
|
||||
onLoad?.()
|
||||
allImagesLoadedState.value = true
|
||||
if (!allImagesLoadedState.value) {
|
||||
onLoad?.()
|
||||
}
|
||||
allImagesLoadedState.value = false
|
||||
setTimeout(() => {
|
||||
allImagesLoadedState.value = true
|
||||
}, 0)
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.on('textureDownloaded', checkIfLoaded)
|
||||
checkIfLoaded()
|
||||
|
|
@ -190,7 +194,7 @@ const renderSlot = (slot: RenderSlot, skipBlock = false): {
|
|||
itemTexture = itemsRenderer.getItemTexture(itemName) ?? itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
} catch (err) {
|
||||
itemTexture = itemsRenderer.getItemTexture('block/errored')!
|
||||
inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.message}`)
|
||||
inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
||||
}
|
||||
if ('type' in itemTexture) {
|
||||
// is item
|
||||
|
|
|
|||
107
src/mineflayer/cameraShake.ts
Normal file
107
src/mineflayer/cameraShake.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
class CameraShake {
|
||||
private rollAngle = 0
|
||||
private get damageRollAmount () { return 5 }
|
||||
private get damageAnimDuration () { return 200 }
|
||||
private rollAnimation?: { startTime: number, startRoll: number, targetRoll: number, duration: number, returnToZero?: boolean }
|
||||
|
||||
constructor () {
|
||||
this.rollAngle = 0
|
||||
}
|
||||
|
||||
shakeFromDamage () {
|
||||
// Add roll animation
|
||||
const startRoll = this.rollAngle
|
||||
const targetRoll = startRoll + (Math.random() < 0.5 ? -1 : 1) * this.damageRollAmount
|
||||
|
||||
this.rollAnimation = {
|
||||
startTime: performance.now(),
|
||||
startRoll,
|
||||
targetRoll,
|
||||
duration: this.damageAnimDuration / 2
|
||||
}
|
||||
}
|
||||
|
||||
update () {
|
||||
// Update roll animation
|
||||
if (this.rollAnimation) {
|
||||
const now = performance.now()
|
||||
const elapsed = now - this.rollAnimation.startTime
|
||||
const progress = Math.min(elapsed / this.rollAnimation.duration, 1)
|
||||
|
||||
if (this.rollAnimation.returnToZero) {
|
||||
// Ease back to zero
|
||||
this.rollAngle = this.rollAnimation.startRoll * (1 - this.easeInOut(progress))
|
||||
if (progress === 1) {
|
||||
this.rollAnimation = undefined
|
||||
}
|
||||
} else {
|
||||
// Initial roll
|
||||
this.rollAngle = this.rollAnimation.startRoll + (this.rollAnimation.targetRoll - this.rollAnimation.startRoll) * this.easeOut(progress)
|
||||
if (progress === 1) {
|
||||
// Start return to zero animation
|
||||
this.rollAnimation = {
|
||||
startTime: now,
|
||||
startRoll: this.rollAngle,
|
||||
targetRoll: 0,
|
||||
duration: this.damageAnimDuration / 2,
|
||||
returnToZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply roll in camera's local space to maintain consistent left/right roll
|
||||
const { camera } = viewer
|
||||
const rollQuat = new THREE.Quaternion()
|
||||
rollQuat.setFromAxisAngle(new THREE.Vector3(0, 0, 1), THREE.MathUtils.degToRad(this.rollAngle))
|
||||
|
||||
// Get camera's current rotation
|
||||
const camQuat = new THREE.Quaternion()
|
||||
camera.getWorldQuaternion(camQuat)
|
||||
|
||||
// Apply roll after camera rotation
|
||||
const finalQuat = camQuat.multiply(rollQuat)
|
||||
camera.setRotationFromQuaternion(finalQuat)
|
||||
}
|
||||
|
||||
private easeOut (t: number): number {
|
||||
return 1 - (1 - t) * (1 - t)
|
||||
}
|
||||
|
||||
private easeInOut (t: number): number {
|
||||
return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2
|
||||
}
|
||||
}
|
||||
|
||||
let cameraShake: CameraShake
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
if (!cameraShake) {
|
||||
cameraShake = new CameraShake()
|
||||
beforeRenderFrame.push(() => {
|
||||
cameraShake.update()
|
||||
})
|
||||
}
|
||||
|
||||
customEvents.on('hurtAnimation', () => {
|
||||
cameraShake.shakeFromDamage()
|
||||
})
|
||||
|
||||
bot._client.on('hurt_animation', () => {
|
||||
customEvents.emit('hurtAnimation')
|
||||
})
|
||||
bot.on('entityHurt', ({ id }) => {
|
||||
if (id === bot.entity.id) {
|
||||
customEvents.emit('hurtAnimation')
|
||||
}
|
||||
})
|
||||
let { health } = bot
|
||||
bot.on('health', () => {
|
||||
if (bot.health < health) {
|
||||
customEvents.emit('hurtAnimation')
|
||||
}
|
||||
health = bot.health
|
||||
})
|
||||
})
|
||||
20
src/mineflayer/maps.ts
Normal file
20
src/mineflayer/maps.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { mapDownloader } from 'mineflayer-item-map-downloader'
|
||||
import { setImageConverter } from 'mineflayer-item-map-downloader/lib/util'
|
||||
|
||||
setImageConverter((buf: Uint8Array) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = 128
|
||||
canvas.height = 128
|
||||
const imageData = ctx.createImageData(canvas.width, canvas.height)
|
||||
imageData.data.set(buf)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
// data url
|
||||
return canvas.toDataURL('image/png')
|
||||
})
|
||||
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
bot.on('login', () => {
|
||||
bot.loadPlugin(mapDownloader)
|
||||
})
|
||||
})
|
||||
|
|
@ -438,21 +438,30 @@ export const guiOptionsScheme: {
|
|||
onClick={() => {
|
||||
packetsReplaceSessionState.active = !active
|
||||
}}
|
||||
>{active ? 'Disable' : 'Enable'} Packets Replay</Button>
|
||||
>{active ? 'Stop' : 'Start'} Packets Replay Logging</Button>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { active } = useSnapshot(packetsReplaceSessionState)
|
||||
const { active, hasRecordedPackets } = useSnapshot(packetsReplaceSessionState)
|
||||
return <Button
|
||||
disabled={!active}
|
||||
disabled={!hasRecordedPackets}
|
||||
inScreen
|
||||
onClick={() => {
|
||||
void downloadPacketsReplay()
|
||||
}}
|
||||
>Download Packets Replay</Button>
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
packetsLoggerPreset: {
|
||||
text: 'Packets Logger Preset',
|
||||
values: [
|
||||
['all', 'All'],
|
||||
['no-buffers', 'No Buffers']
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR'
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ const defaultOptions = {
|
|||
useVersionsTextures: 'latest',
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
handDisplay: false,
|
||||
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
|
||||
|
||||
// antiAliasing: false,
|
||||
|
||||
|
|
@ -93,7 +94,7 @@ const defaultOptions = {
|
|||
minimapOptimizations: true,
|
||||
displayBossBars: false, // boss bar overlay was removed for some reason, enable safely
|
||||
disabledUiParts: [] as string[],
|
||||
neighborChunkUpdates: true
|
||||
neighborChunkUpdates: true,
|
||||
}
|
||||
|
||||
function getDefaultTouchControlsPositions () {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,60 @@
|
|||
import { proxy } from 'valtio'
|
||||
import { PacketsLogger } from './packetsReplayBase'
|
||||
import { options } from './optionsStorage'
|
||||
|
||||
export const packetsReplaceSessionState = proxy({
|
||||
active: false,
|
||||
hasRecordedPackets: false
|
||||
})
|
||||
|
||||
const replayLogger = new PacketsLogger()
|
||||
export const replayLogger = new PacketsLogger()
|
||||
|
||||
const isBufferData = (data: any): boolean => {
|
||||
if (Buffer.isBuffer(data) || data instanceof Uint8Array) return true
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
return Object.values(data).some(value => isBufferData(value))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const processPacketData = (data: any): any => {
|
||||
if (options.packetsLoggerPreset === 'no-buffers') {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return '[buffer]'
|
||||
}
|
||||
if (typeof data === 'object' && data !== null) {
|
||||
const processed = {}
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
processed[key] = isBufferData(value) ? '[buffer]' : value
|
||||
}
|
||||
return processed
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export default () => {
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
replayLogger.contents = ''
|
||||
bot._client.on('packet', (data, { name, state }) => {
|
||||
packetsReplaceSessionState.hasRecordedPackets = false
|
||||
const handleServerPacket = (data, { name, state = bot._client.state }) => {
|
||||
if (!packetsReplaceSessionState.active) {
|
||||
return
|
||||
}
|
||||
replayLogger.log(true, { name, state }, data)
|
||||
replayLogger.log(true, { name, state }, processPacketData(data))
|
||||
packetsReplaceSessionState.hasRecordedPackets = true
|
||||
}
|
||||
bot._client.on('packet', handleServerPacket)
|
||||
bot._client.on('packet_name' as any, (name, data) => {
|
||||
handleServerPacket(data, { name })
|
||||
})
|
||||
|
||||
bot._client.on('writePacket' as any, (name, data) => {
|
||||
if (!packetsReplaceSessionState.active) {
|
||||
return
|
||||
}
|
||||
replayLogger.log(false, { name, state: bot._client.state }, data)
|
||||
replayLogger.log(false, { name, state: bot._client.state }, processPacketData(data))
|
||||
packetsReplaceSessionState.hasRecordedPackets = true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -30,3 +65,4 @@ export const downloadPacketsReplay = async () => {
|
|||
a.download = `packets-replay-${new Date().toISOString()}.txt`
|
||||
a.click()
|
||||
}
|
||||
globalThis.downloadPacketsReplay = downloadPacketsReplay
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import Screen from './Screen'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
|
|
@ -26,11 +26,12 @@ interface Props {
|
|||
accounts?: string[]
|
||||
authenticatedAccounts?: number
|
||||
versions?: string[]
|
||||
allowAutoConnect?: boolean
|
||||
}
|
||||
|
||||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, authenticatedAccounts }: Props) => {
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||
const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined
|
||||
const qsParamName = qsParams?.get('name')
|
||||
const qsParamIp = qsParams?.get('ip')
|
||||
|
|
@ -40,7 +41,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
const qsParamLockConnect = qsParams?.get('lockConnect')
|
||||
|
||||
const qsIpParts = qsParamIp?.split(':')
|
||||
const ipParts = initialData?.ip.split(':')
|
||||
const ipParts = initialData?.ip ? initialData?.ip.split(':') : undefined
|
||||
|
||||
const [serverName, setServerName] = React.useState(initialData?.name ?? qsParamName ?? '')
|
||||
const [serverIp, setServerIp] = React.useState(ipParts?.[0] ?? qsIpParts?.[0] ?? '')
|
||||
|
|
@ -69,6 +70,12 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
authenticatedAccountOverride,
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (qsParams?.get('autoConnect') === 'true' && qsParams?.get('ip') && allowAutoConnect) {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Screen title={qsParamIp ? 'Connect to Server' : title} backdrop>
|
||||
<form
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { resetLocalStorageWorld } from '../browserfs'
|
|||
import { fsState } from '../loadSave'
|
||||
import { guessProblem } from '../errorLoadingScreenHelpers'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { downloadPacketsReplay, packetsReplaceSessionState } from '../packetsReplay'
|
||||
import { downloadPacketsReplay, packetsReplaceSessionState, replayLogger } from '../packetsReplay'
|
||||
import { getProxyDetails } from '../microsoftAuthflow'
|
||||
import AppStatus from './AppStatus'
|
||||
import DiveTransition from './DiveTransition'
|
||||
|
|
@ -121,7 +121,7 @@ export default () => {
|
|||
<>
|
||||
{displayAuthButton && <Button label='Authenticate' onClick={authReconnectAction} />}
|
||||
{displayVpnButton && <PossiblyVpnBypassProxyButton reconnect={reconnect} />}
|
||||
{replayActive && <Button label='Download Packets Replay' onClick={downloadPacketsReplay} />}
|
||||
{replayActive && <Button label={`Download Packets Replay ${replayLogger.contents.split('\n').length}L`} onClick={downloadPacketsReplay} />}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import PixelartIcon from './PixelartIcon'
|
|||
|
||||
interface Props extends React.ComponentProps<'button'> {
|
||||
label?: string
|
||||
postLabel?: React.ReactNode
|
||||
icon?: string
|
||||
children?: React.ReactNode
|
||||
inScreen?: boolean
|
||||
|
|
@ -22,7 +23,7 @@ export const ButtonProvider: FC<{ children, onClick }> = ({ children, onClick })
|
|||
return <ButtonContext.Provider value={{ onClick }}>{children}</ButtonContext.Provider>
|
||||
}
|
||||
|
||||
export default (({ label, icon, children, inScreen, rootRef, type = 'button', ...args }) => {
|
||||
export default (({ label, icon, children, inScreen, rootRef, type = 'button', postLabel, ...args }) => {
|
||||
const ctx = useContext(ButtonContext)
|
||||
|
||||
const onClick = (e) => {
|
||||
|
|
@ -42,6 +43,7 @@ export default (({ label, icon, children, inScreen, rootRef, type = 'button', ..
|
|||
<button ref={rootRef} {...args} className={classNames(buttonCss.button, args.className)} onClick={onClick} type={type}>
|
||||
{icon && <PixelartIcon className={buttonCss.icon} iconName={icon} />}
|
||||
{label}
|
||||
{postLabel}
|
||||
{children}
|
||||
</button>
|
||||
</SharedHudVars>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,16 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { mapDownloader } from 'mineflayer-item-map-downloader/'
|
||||
import { setImageConverter } from 'mineflayer-item-map-downloader/lib/util'
|
||||
|
||||
export default () => {
|
||||
const [dataUrl, setDataUrl] = useState<string | null | true>(null) // true means loading
|
||||
|
||||
useEffect(() => {
|
||||
bot.loadPlugin(mapDownloader)
|
||||
|
||||
setImageConverter((buf: Uint8Array) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')!
|
||||
canvas.width = 128
|
||||
canvas.height = 128
|
||||
const imageData = ctx.createImageData(canvas.width, canvas.height)
|
||||
imageData.data.set(buf)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
// data url
|
||||
return canvas.toDataURL('image/png')
|
||||
})
|
||||
|
||||
// TODO delete maps!
|
||||
const updateHeldMap = () => {
|
||||
setDataUrl(null)
|
||||
if (!bot.heldItem || !['filled_map', 'map'].includes(bot.heldItem.name)) return
|
||||
const item = bot.heldItem
|
||||
if (!item || !['filled_map', 'map'].includes(item.name)) return
|
||||
// setDataUrl(true)
|
||||
const mapNumber = (bot.heldItem?.nbt?.value as any)?.map?.value
|
||||
const mapNumber = ((item?.nbt?.value as any)?.map?.value) ?? (item['components']?.find(x => x.type === 'map_id')?.data)
|
||||
// if (!mapNumber) return
|
||||
setDataUrl(bot.mapDownloader.maps?.[mapNumber] as unknown as string)
|
||||
}
|
||||
|
|
@ -38,6 +23,8 @@ export default () => {
|
|||
// total maps: Object.keys(bot.mapDownloader.maps).length
|
||||
updateHeldMap()
|
||||
})
|
||||
|
||||
updateHeldMap()
|
||||
}, [])
|
||||
|
||||
return dataUrl && dataUrl !== true ? <div style={{
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
|
|||
const valuesTitlesMap = useMemo(() => {
|
||||
if (!item.values) {
|
||||
return {
|
||||
// true: <span style={{ color: 'lime' }}>ON</span>,
|
||||
// false: <span style={{ color: 'red' }}>OFF</span>,
|
||||
true: 'ON',
|
||||
false: 'OFF',
|
||||
}
|
||||
|
|
@ -82,7 +84,9 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
|
|||
return <Button
|
||||
data-setting={item.id}
|
||||
label={`${item.text}: ${valuesTitlesMap[optionValue]}`}
|
||||
onClick={async () => {
|
||||
// label={`${item.text}:`}
|
||||
// postLabel={valuesTitlesMap[optionValue]}
|
||||
onClick={async (event) => {
|
||||
if (disabledReason) {
|
||||
await showOptionsModal(`The option is unavailable. ${disabledReason}`, [])
|
||||
return
|
||||
|
|
@ -106,7 +110,10 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
|
|||
if (currentIndex === -1) {
|
||||
options[item.id!] = getOptionValue(values[0])
|
||||
} else {
|
||||
options[item.id!] = getOptionValue(values[(currentIndex + 1) % values.length])
|
||||
const nextIndex = event.shiftKey
|
||||
? (currentIndex - 1 + values.length) % values.length
|
||||
: (currentIndex + 1) % values.length
|
||||
options[item.id!] = getOptionValue(values[nextIndex])
|
||||
}
|
||||
} else {
|
||||
options[item.id!] = !options[item.id!]
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
||||
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
|
|
@ -252,6 +252,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
})
|
||||
|
||||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
|
||||
placeholders={{
|
||||
proxyOverride: selectedProxy,
|
||||
usernameOverride: defaultUsername,
|
||||
|
|
@ -412,6 +413,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
onRowSelect={(_, i) => {
|
||||
setSelectedIndex(i)
|
||||
}}
|
||||
selectedRow={selectedIndex}
|
||||
/>
|
||||
return <>
|
||||
{serversListJsx}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,10 @@ export interface WorldProps {
|
|||
worldNameRight?: string
|
||||
onFocus?: (name: string) => void
|
||||
onInteraction?(interaction: 'enter' | 'space')
|
||||
elemRef?: React.Ref<HTMLDivElement>
|
||||
}
|
||||
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight }: WorldProps) => {
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
const timeRelativeFormatted = useMemo(() => {
|
||||
if (!lastPlayed) return ''
|
||||
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||
|
|
@ -47,6 +48,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
}, [size])
|
||||
|
||||
return <div
|
||||
ref={elemRef}
|
||||
className={classNames(styles.world_root, isFocused ? styles.world_focused : undefined)} tabIndex={0} onFocus={() => onFocus?.(name)} onKeyDown={(e) => {
|
||||
if (e.code === 'Enter' || e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
|
|
@ -92,6 +94,7 @@ interface Props {
|
|||
onGeneralAction (action: 'cancel' | 'create'): void
|
||||
onRowSelect? (name: string, index: number): void
|
||||
defaultSelectedRow?: number
|
||||
selectedRow?: number
|
||||
listStyle?: React.CSSProperties
|
||||
setListHovered?: (hovered: boolean) => void
|
||||
secondRowStyles?: React.CSSProperties
|
||||
|
|
@ -116,6 +119,7 @@ export default ({
|
|||
hidden,
|
||||
onRowSelect,
|
||||
defaultSelectedRow,
|
||||
selectedRow,
|
||||
listStyle,
|
||||
setListHovered,
|
||||
secondRowStyles,
|
||||
|
|
@ -123,9 +127,10 @@ export default ({
|
|||
}: Props) => {
|
||||
const containerRef = useRef<any>()
|
||||
const firstButton = useRef<HTMLButtonElement>(null)
|
||||
const worldRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
|
||||
useTypedEventListener(window, 'keydown', (e) => {
|
||||
if ((e.code === 'ArrowDown' || e.code === 'ArrowUp') && e.ctrlKey) {
|
||||
if ((e.code === 'ArrowDown' || e.code === 'ArrowUp')) {
|
||||
e.preventDefault()
|
||||
const dir = e.code === 'ArrowDown' ? 1 : -1
|
||||
const elements = focusable(containerRef.current)
|
||||
|
|
@ -137,12 +142,21 @@ export default ({
|
|||
})
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [focusedWorld, setFocusedWorld] = useState(defaultSelectedRow ? worldData?.[defaultSelectedRow]?.name ?? '' : '')
|
||||
const [focusedWorld, setFocusedWorld] = useState(defaultSelectedRow === undefined ? '' : worldData?.[defaultSelectedRow]?.name ?? '')
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedWorld('')
|
||||
}, [activeProvider])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRow === undefined) return
|
||||
const worldName = worldData?.[selectedRow]?.name
|
||||
setFocusedWorld(worldName ?? '')
|
||||
if (worldName) {
|
||||
worldRefs.current[worldName]?.focus()
|
||||
}
|
||||
}, [selectedRow, worldData?.[selectedRow as any]?.name])
|
||||
|
||||
const onRowSelectHandler = (name: string, index: number) => {
|
||||
onRowSelect?.(name, index)
|
||||
setFocusedWorld(name)
|
||||
|
|
@ -186,7 +200,14 @@ export default ({
|
|||
worldData
|
||||
? worldData.filter(data => data.title.toLowerCase().includes(search.toLowerCase())).map(({ name, size, detail, ...rest }, index) => (
|
||||
<World
|
||||
{...rest} size={size} name={name} onFocus={row => onRowSelectHandler(row, index)} isFocused={focusedWorld === name} key={name} onInteraction={(interaction) => {
|
||||
{...rest}
|
||||
size={size}
|
||||
name={name}
|
||||
elemRef={el => { worldRefs.current[name] = el }}
|
||||
onFocus={row => onRowSelectHandler(row, index)}
|
||||
isFocused={focusedWorld === name}
|
||||
key={name}
|
||||
onInteraction={(interaction) => {
|
||||
if (interaction === 'enter') onWorldAction('load', name)
|
||||
else if (interaction === 'space') firstButton.current?.focus()
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ import { options } from './optionsStorage'
|
|||
export const watchFov = () => {
|
||||
const updateFov = () => {
|
||||
if (!bot) return
|
||||
let fovSetting = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
let fov = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
// todo check values and add transition
|
||||
if (bot.controlState.sprint && !bot.controlState.sneak) {
|
||||
fovSetting += 5
|
||||
fov += 5
|
||||
}
|
||||
if (gameAdditionalState.isFlying) {
|
||||
fovSetting += 5
|
||||
fov += 5
|
||||
}
|
||||
viewer.camera.fov = fovSetting
|
||||
viewer.camera.fov = fov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
updateFov()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
/* eslint-disable no-await-in-loop */
|
||||
import { join, dirname, basename } from 'path'
|
||||
import fs from 'fs'
|
||||
import JSZip from 'jszip'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import { mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
|
|
@ -9,7 +11,8 @@ import { options } from './optionsStorage'
|
|||
import { showOptionsModal } from './react/SelectOption'
|
||||
import { appStatusState } from './react/AppStatusProvider'
|
||||
import { appReplacableResources, resourcesContentOriginal } from './generated/resources'
|
||||
import { loadedGameState } from './globalState'
|
||||
import { loadedGameState, miscUiState } from './globalState'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
|
||||
export const resourcePackState = proxy({
|
||||
resourcePackInstalled: false,
|
||||
|
|
@ -169,64 +172,106 @@ export const getActiveTexturepackBasePath = async () => {
|
|||
return null
|
||||
}
|
||||
|
||||
const isDirSafe = async (filePath: string) => {
|
||||
try {
|
||||
return await fs.promises.stat(filePath).then(stat => stat.isDirectory()).catch(() => false)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const getFilesMapFromDir = async (dir: string) => {
|
||||
const files = [] as string[]
|
||||
const scan = async (dir) => {
|
||||
const dirFiles = await fs.promises.readdir(dir)
|
||||
for (const file of dirFiles) {
|
||||
const filePath = join(dir, file)
|
||||
if (await isDirSafe(filePath)) {
|
||||
await scan(filePath)
|
||||
} else {
|
||||
files.push(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
await scan(dir)
|
||||
return files
|
||||
}
|
||||
|
||||
export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
if (!basePath) return
|
||||
const texturesCommonBasePath = `${basePath}/assets/minecraft/textures`
|
||||
let texturesBasePath = `${texturesCommonBasePath}/${type === 'blocks' ? 'block' : 'item'}`
|
||||
const texturesBasePathAlt = `${texturesCommonBasePath}/${type === 'blocks' ? 'blocks' : 'items'}`
|
||||
if (!(await existsAsync(texturesBasePath))) {
|
||||
if (await existsAsync(texturesBasePathAlt)) {
|
||||
texturesBasePath = texturesBasePathAlt
|
||||
}
|
||||
}
|
||||
const allInterestedPaths = existingTextures.map(tex => {
|
||||
if (tex.includes('/')) {
|
||||
return join(`${texturesCommonBasePath}/${tex}`)
|
||||
}
|
||||
return join(texturesBasePath, tex)
|
||||
})
|
||||
const allInterestedPathsPerDir = new Map<string, string[]>()
|
||||
for (const path of allInterestedPaths) {
|
||||
const dir = dirname(path)
|
||||
if (!allInterestedPathsPerDir.has(dir)) {
|
||||
allInterestedPathsPerDir.set(dir, [])
|
||||
}
|
||||
const file = basename(path)
|
||||
allInterestedPathsPerDir.get(dir)!.push(file)
|
||||
}
|
||||
// filter out by readdir each dir
|
||||
const allInterestedImages = [] as string[]
|
||||
for (const [dir, paths] of allInterestedPathsPerDir) {
|
||||
if (!await existsAsync(dir)) {
|
||||
continue
|
||||
}
|
||||
const dirImages = (await fs.promises.readdir(dir)).filter(f => f.endsWith('.png')).map(f => f.replace('.png', ''))
|
||||
allInterestedImages.push(...dirImages.filter(image => paths.includes(image)).map(image => `${dir}/${image}`))
|
||||
}
|
||||
|
||||
if (allInterestedImages.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let firstTextureSize: number | undefined
|
||||
const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
|
||||
if (appStatusState.status) {
|
||||
setLoadingScreenStatus(`Generating atlas texture for ${type}`)
|
||||
}
|
||||
const textures = {} as Record<string, HTMLImageElement>
|
||||
for (const namespace of namespaces) {
|
||||
const texturesCommonBasePath = `${basePath}/assets/${namespace}/textures`
|
||||
const isMinecraftNamespace = namespace === 'minecraft'
|
||||
let texturesBasePath = `${texturesCommonBasePath}/${type === 'blocks' ? 'block' : 'item'}`
|
||||
const texturesBasePathAlt = `${texturesCommonBasePath}/${type === 'blocks' ? 'blocks' : 'items'}`
|
||||
if (!(await existsAsync(texturesBasePath))) {
|
||||
if (await existsAsync(texturesBasePathAlt)) {
|
||||
texturesBasePath = texturesBasePathAlt
|
||||
}
|
||||
}
|
||||
const allInterestedPaths = new Set(
|
||||
existingTextures
|
||||
.filter(tex => (isMinecraftNamespace && !tex.includes(':')) || (tex.includes(':') && tex.split(':')[0] === namespace))
|
||||
.map(tex => {
|
||||
tex = tex.split(':')[1] ?? tex
|
||||
if (tex.includes('/')) {
|
||||
return join(`${texturesCommonBasePath}/${tex}`)
|
||||
}
|
||||
return join(texturesBasePath, tex)
|
||||
})
|
||||
)
|
||||
// add all files from texturesCommonBasePath
|
||||
// if (!isMinecraftNamespace) {
|
||||
// const commonBasePathFiles = await getFilesMapFromDir(texturesCommonBasePath)
|
||||
// for (const file of commonBasePathFiles) {
|
||||
// allInterestedPaths.add(file)
|
||||
// }
|
||||
// }
|
||||
const allInterestedPathsPerDir = new Map<string, string[]>()
|
||||
for (const path of allInterestedPaths) {
|
||||
const dir = dirname(path)
|
||||
if (!allInterestedPathsPerDir.has(dir)) {
|
||||
allInterestedPathsPerDir.set(dir, [])
|
||||
}
|
||||
const file = basename(path)
|
||||
allInterestedPathsPerDir.get(dir)!.push(file)
|
||||
}
|
||||
// filter out by readdir each dir
|
||||
const allInterestedImages = [] as string[]
|
||||
for (const [dir, paths] of allInterestedPathsPerDir) {
|
||||
if (!await existsAsync(dir)) {
|
||||
continue
|
||||
}
|
||||
const dirImages = (await fs.promises.readdir(dir)).filter(f => f.endsWith('.png')).map(f => f.replace('.png', ''))
|
||||
allInterestedImages.push(...dirImages.filter(image => paths.includes(image)).map(image => `${dir}/${image}`))
|
||||
}
|
||||
|
||||
const firstImageFile = allInterestedImages[0]!
|
||||
if (allInterestedImages.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
let firstTextureSize: number | undefined
|
||||
try {
|
||||
// todo compare sizes from atlas
|
||||
firstTextureSize = await getSizeFromImage(`${firstImageFile}.png`)
|
||||
} catch (err) { }
|
||||
const textures = Object.fromEntries(await Promise.all(allInterestedImages.map(async (image) => {
|
||||
const imagePath = `${image}.png`
|
||||
const contents = await fs.promises.readFile(imagePath, 'base64')
|
||||
const img = await getLoadedImage(`data:image/png;base64,${contents}`)
|
||||
const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '')
|
||||
return [imageRelative, img]
|
||||
})))
|
||||
const firstImageFile = allInterestedImages[0]!
|
||||
try {
|
||||
// todo check all sizes from atlas
|
||||
firstTextureSize ??= await getSizeFromImage(`${firstImageFile}.png`)
|
||||
} catch (err) { }
|
||||
const newTextures = Object.fromEntries(await Promise.all(allInterestedImages.map(async (image) => {
|
||||
const imagePath = `${image}.png`
|
||||
const contents = await fs.promises.readFile(imagePath, 'base64')
|
||||
const img = await getLoadedImage(`data:image/png;base64,${contents}`)
|
||||
const imageRelative = image.replace(`${texturesBasePath}/`, '').replace(`${texturesCommonBasePath}/`, '')
|
||||
const textureName = isMinecraftNamespace ? imageRelative : `${namespace}:${imageRelative}`
|
||||
return [textureName, img]
|
||||
})))
|
||||
Object.assign(textures, newTextures) as any
|
||||
}
|
||||
return {
|
||||
firstTextureSize,
|
||||
textures
|
||||
|
|
@ -234,8 +279,9 @@ export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTex
|
|||
}
|
||||
|
||||
const prepareBlockstatesAndModels = async () => {
|
||||
viewer.world.customBlockStates = undefined
|
||||
viewer.world.customModels = undefined
|
||||
viewer.world.customBlockStates = {}
|
||||
viewer.world.customModels = {}
|
||||
const usedTextures = new Set<string>()
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
if (!basePath) return
|
||||
if (appStatusState.status) {
|
||||
|
|
@ -256,16 +302,25 @@ const prepareBlockstatesAndModels = async () => {
|
|||
if (type === 'models') {
|
||||
name = `block/${name}`
|
||||
}
|
||||
if (namespaceDir !== 'minecraft') {
|
||||
name = `${namespaceDir}:${name}`
|
||||
const parsed = JSON.parse(contents)
|
||||
if (namespaceDir === 'minecraft') {
|
||||
jsons[name] = parsed
|
||||
}
|
||||
jsons[`${namespaceDir}:${name}`] = parsed
|
||||
if (type === 'models') {
|
||||
for (let texturePath of Object.values(parsed.textures ?? {})) {
|
||||
if (typeof texturePath !== 'string') continue
|
||||
if (texturePath.startsWith('#')) continue
|
||||
if (!texturePath.includes(':')) texturePath = `minecraft:${texturePath}`
|
||||
usedTextures.add(texturePath as string)
|
||||
}
|
||||
}
|
||||
jsons[name] = JSON.parse(contents)
|
||||
}
|
||||
}))
|
||||
return jsons
|
||||
}
|
||||
viewer.world.customBlockStates = await getAllJson(blockstatesPath, 'blockstates')
|
||||
viewer.world.customModels = await getAllJson(modelsPath, 'models')
|
||||
Object.assign(viewer.world.customBlockStates!, await getAllJson(blockstatesPath, 'blockstates'))
|
||||
Object.assign(viewer.world.customModels!, await getAllJson(modelsPath, 'models'))
|
||||
}
|
||||
try {
|
||||
const assetsDirs = await fs.promises.readdir(join(basePath, 'assets'))
|
||||
|
|
@ -277,6 +332,7 @@ const prepareBlockstatesAndModels = async () => {
|
|||
viewer.world.customBlockStates = undefined
|
||||
viewer.world.customModels = undefined
|
||||
}
|
||||
return { usedTextures }
|
||||
}
|
||||
|
||||
const downloadAndUseResourcePack = async (url: string): Promise<void> => {
|
||||
|
|
@ -290,6 +346,17 @@ const downloadAndUseResourcePack = async (url: string): Promise<void> => {
|
|||
})
|
||||
}
|
||||
|
||||
const waitForGameEvent = async () => {
|
||||
if (miscUiState.gameLoaded) return
|
||||
await new Promise<void>(resolve => {
|
||||
const listener = () => resolve()
|
||||
customEvents.once('gameLoaded', listener)
|
||||
watchUnloadForCleanup(() => {
|
||||
customEvents.removeListener('gameLoaded', listener)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const onAppLoad = () => {
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
// todo also handle resourcePack
|
||||
|
|
@ -307,8 +374,12 @@ export const onAppLoad = () => {
|
|||
minecraftJsonMessage: promptMessagePacket,
|
||||
})
|
||||
if (!choice) return
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500)
|
||||
})
|
||||
console.log('accepting resource pack')
|
||||
bot.acceptResourcePack()
|
||||
if (choice === 'Download & Install (recommended)') {
|
||||
if (choice === true || choice === 'Download & Install (recommended)') {
|
||||
await downloadAndUseResourcePack(packet.url).catch((err) => {
|
||||
console.error(err)
|
||||
showNotification('Failed to download resource pack: ' + err.message)
|
||||
|
|
@ -349,10 +420,10 @@ const updateAllReplacableTextures = async () => {
|
|||
for (const [key, { cssVar, cssVarRepeat, resourcePackPath }] of vars) {
|
||||
const resPath = `${basePath}/assets/${resourcePackPath}`
|
||||
if (cssVar) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
await setCustomCss(resPath, cssVar, cssVarRepeat ?? 1)
|
||||
} else {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
await setCustomPicture(key, resPath)
|
||||
}
|
||||
}
|
||||
|
|
@ -363,10 +434,10 @@ const repeatArr = (arr, i) => Array.from({ length: i }, () => arr)
|
|||
const updateTextures = async () => {
|
||||
const blocksFiles = Object.keys(viewer.world.blocksAtlases.latest.textures)
|
||||
const itemsFiles = Object.keys(viewer.world.itemsAtlases.latest.textures)
|
||||
const blocksData = await getResourcepackTiles('blocks', blocksFiles)
|
||||
const { usedTextures: extraBlockTextures = new Set<string>() } = await prepareBlockstatesAndModels() ?? {}
|
||||
const blocksData = await getResourcepackTiles('blocks', [...blocksFiles, ...extraBlockTextures])
|
||||
const itemsData = await getResourcepackTiles('items', itemsFiles)
|
||||
await updateAllReplacableTextures()
|
||||
await prepareBlockstatesAndModels()
|
||||
viewer.world.customTextures = {}
|
||||
if (blocksData) {
|
||||
viewer.world.customTextures.blocks = {
|
||||
|
|
@ -382,6 +453,9 @@ const updateTextures = async () => {
|
|||
}
|
||||
if (viewer.world.active) {
|
||||
await viewer.world.updateTexturesData()
|
||||
if (viewer.world instanceof WorldRendererThree) {
|
||||
viewer.world.rerenderAllChunks?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { supportedVersions } from 'minecraft-data'
|
||||
import { supportedVersions, postNettyVersionsByProtocolVersion } from 'minecraft-data'
|
||||
|
||||
const ignoredVersionsRegex = /(^0\.30c$)|w|-pre|-rc/
|
||||
export const ignoredVersionsRegex = /(^0\.30c$)|w|-pre|-rc/
|
||||
|
||||
export default supportedVersions.pc.filter(x => x !== '1.7').filter(v => !ignoredVersionsRegex.test(v))
|
||||
/** @type {string[]} */
|
||||
const versionsFromProtocol = Object.values(postNettyVersionsByProtocolVersion.pc).flat().filter(x => !ignoredVersionsRegex.test(x.minecraftVersion)).map(x => x.minecraftVersion)
|
||||
|
||||
export default versionsFromProtocol.filter(x => x !== '1.7' && !x.startsWith('1.7.'))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue