1+ diff --git a/src/client.js b/src/client.js
2+ index e369e77..f6c408d 100644
3+ --- a/src/client.js
4+ +++ b/src/client.js
5+ @@ -75,7 +75,35 @@ class Client extends EventEmitter {
6+ }
7+ const deserializerDirection = this.isServer ? 'toServer' : 'toClient'
8+ e.field = [this.protocolState, deserializerDirection].concat(parts).join('.')
9+ - e.message = e.buffer ? `Parse error for ${e.field} (${e.buffer?.length} bytes, ${e.buffer?.toString('hex').slice(0, 6)}...) : ${e.message}` : `Parse error for ${e.field}: ${e.message}`
10+ + e.message = e.buffer
11+ + ? `Parse error for ${e.field} (${e.buffer?.length} bytes, ${e.buffer?.toString('hex').slice(0, 6)}...) : ${e.message}`
12+ + : `Parse error for ${e.field}: ${e.message}`
13+ +
14+ + // Non-fatal play-state parse error — e.g. Hypixel sending NBT tag IDs outside
15+ + // the standard 0-20 range (tag 71 = custom component data introduced in 1.21.4+).
16+ + // The deserializer stream is destroyed by Node.js on error, so we tear it down
17+ + // fully, rebuild it via setSerializer, and re-pipe it into the chain so packet
18+ + // processing continues normally. We emit 'warning' instead of 'error' so nothing
19+ + // upstream treats this as a fatal connection failure.
20+ + if (this.protocolState === states.PLAY) {
21+ + console.warn(`[minecraft-protocol] Non-fatal parse error (connection kept alive): ${e.message}`)
22+ + this.deserializer.removeAllListeners()
23+ + if (!this.compressor) {
24+ + this.splitter.unpipe(this.deserializer)
25+ + } else {
26+ + this.decompressor.unpipe(this.deserializer)
27+ + }
28+ + this.setSerializer(states.PLAY)
29+ + if (!this.compressor) {
30+ + this.splitter.pipe(this.deserializer)
31+ + } else {
32+ + this.decompressor.pipe(this.deserializer)
33+ + }
34+ + this.emit('warning', e)
35+ + return
36+ + }
37+ +
38+ + // All other parse errors are still fatal — re-pipe attempt then propagate.
39+ if (!this.compressor) { this.splitter.pipe(this.deserializer) } else { this.decompressor.pipe(this.deserializer) }
40+ this.emit('error', e)
41+ })
42+ @@ -111,7 +139,13 @@ class Client extends EventEmitter {
43+ this._hasBundlePacket = false
44+ }
45+ } else {
46+ - emitPacket(parsed)
47+ + try {
48+ + emitPacket(parsed)
49+ + } catch (err) {
50+ + console.log('Client incorrectly handled packet ' + parsed.metadata.name)
51+ + console.error(err)
52+ + // todo investigate why it doesn't close the stream even if unhandled there
53+ + }
54+ }
55+ })
56+ }
57+ @@ -169,7 +203,10 @@ class Client extends EventEmitter {
58+ }
59+
60+ const onFatalError = (err) => {
61+ - this.emit('error', err)
62+ + // todo find out what is trying to write after client disconnect
63+ + if(err.code !== 'ECONNABORTED') {
64+ + this.emit('error', err)
65+ + }
66+ endSocket()
67+ }
68+
69+ @@ -198,6 +235,10 @@ class Client extends EventEmitter {
70+ serializer -> framer -> socket -> splitter -> deserializer */
71+ if (this.serializer) {
72+ this.serializer.end()
73+ + setTimeout(() => {
74+ + this.socket?.end()
75+ + this.socket?.emit('end')
76+ + }, 2000) // allow the serializer to finish writing
77+ } else {
78+ if (this.socket) this.socket.end()
79+ }
80+ @@ -243,6 +284,7 @@ class Client extends EventEmitter {
81+ debug('writing packet ' + this.state + '.' + name)
82+ debug(params)
83+ }
84+ + this.emit('writePacket', name, params)
85+ this.serializer.write({ name, params })
86+ }
87+
188diff --git a/src/client/chat.js b/src/client/chat.js
2- index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
89+ index 0021870..a53fceb 100644
390--- a/src/client/chat.js
491+++ b/src/client/chat.js
592@@ -116,7 +116,7 @@ module.exports = function (client, options) {
@@ -57,7 +144,7 @@ index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b
57144 previousMessages: client._lastSeenMessages.map((e) => ({
58145 messageSender: e.sender,
59146diff --git a/src/client/encrypt.js b/src/client/encrypt.js
60- index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644
147+ index 63cc2bd..36df57d 100644
61148--- a/src/client/encrypt.js
62149+++ b/src/client/encrypt.js
63150@@ -25,7 +25,11 @@ module.exports = function (client, options) {
@@ -73,8 +160,72 @@ index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f8
73160 }
74161
75162 function onJoinServerResponse (err) {
163+ diff --git a/src/client/play.js b/src/client/play.js
164+ index 71ad739..7130b22 100644
165+ --- a/src/client/play.js
166+ +++ b/src/client/play.js
167+ @@ -31,11 +31,37 @@ module.exports = function (client, options) {
168+
169+ client.once('success', onLogin)
170+
171+ + function pickLocale () {
172+ + const locales = [
173+ + ['en_US', 0.60],
174+ + ['en_GB', 0.10],
175+ + ['de_DE', 0.12],
176+ + ['id_ID', 0.10],
177+ + ['fr_FR', 0.04],
178+ + ['es_ES', 0.04]
179+ + ]
180+ + const roll = Math.random()
181+ + let cumulative = 0
182+ + for (const [locale, weight] of locales) {
183+ + cumulative += weight
184+ + if (roll < cumulative) return locale
185+ + }
186+ + return 'en_US'
187+ + }
188+ +
189+ function onLogin (packet) {
190+ const mcData = require('minecraft-data')(client.version)
191+ client.uuid = packet.uuid
192+ client.username = packet.username
193+
194+ + const resolvedProfile = {
195+ + locale: options.clientProfile?.locale ?? pickLocale(),
196+ + viewDistance: options.clientProfile?.viewDistance ?? 10,
197+ + mainHand: options.clientProfile?.mainHand !== undefined
198+ + ? options.clientProfile.mainHand
199+ + : (Math.random() < 0.75 ? 1 : 0)
200+ + }
201+ +
202+ if (mcData.supportFeature('hasConfigurationState')) {
203+ client.write('login_acknowledged', {})
204+ enterConfigState(onReady)
205+ @@ -53,6 +79,21 @@ module.exports = function (client, options) {
206+ client.write('configuration_acknowledged', {})
207+ }
208+ client.state = states.CONFIGURATION
209+ +
210+ + // Send client information immediately on entering config phase.
211+ + // Some servers *cough cough hypixel* require this before they will proceed past
212+ + // configuration state. Without it, you get the socketClosed error.
213+ + client.write('settings', {
214+ + locale: resolvedProfile.locale,
215+ + viewDistance: resolvedProfile.viewDistance,
216+ + chatFlags: 0,
217+ + chatColors: true,
218+ + skinParts: 127,
219+ + mainHand: resolvedProfile.mainHand,
220+ + enableTextFiltering: false,
221+ + enableServerListing: true
222+ + })
223+ +
224+ client.once('select_known_packs', () => {
225+ client.write('select_known_packs', { packs: [] })
226+ })
76227diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
77- index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
228+ index 671eb45..7f69f51 100644
78229--- a/src/client/pluginChannels.js
79230+++ b/src/client/pluginChannels.js
80231@@ -57,7 +57,7 @@ module.exports = function (client, options) {
@@ -86,53 +237,3 @@ index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49b
86237 return
87238 }
88239 }
89- diff --git a/src/client.js b/src/client.js
90- index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
91- --- a/src/client.js
92- +++ b/src/client.js
93- @@ -111,7 +111,13 @@ class Client extends EventEmitter {
94- this._hasBundlePacket = false
95- }
96- } else {
97- - emitPacket(parsed)
98- + try {
99- + emitPacket(parsed)
100- + } catch (err) {
101- + console.log('Client incorrectly handled packet ' + parsed.metadata.name)
102- + console.error(err)
103- + // todo investigate why it doesn't close the stream even if unhandled there
104- + }
105- }
106- })
107- }
108- @@ -169,7 +175,10 @@ class Client extends EventEmitter {
109- }
110-
111- const onFatalError = (err) => {
112- - this.emit('error', err)
113- + // todo find out what is trying to write after client disconnect
114- + if(err.code !== 'ECONNABORTED') {
115- + this.emit('error', err)
116- + }
117- endSocket()
118- }
119-
120- @@ -198,6 +207,10 @@ class Client extends EventEmitter {
121- serializer -> framer -> socket -> splitter -> deserializer */
122- if (this.serializer) {
123- this.serializer.end()
124- + setTimeout(() => {
125- + this.socket?.end()
126- + this.socket?.emit('end')
127- + }, 2000) // allow the serializer to finish writing
128- } else {
129- if (this.socket) this.socket.end()
130- }
131- @@ -243,6 +256,7 @@ class Client extends EventEmitter {
132- debug('writing packet ' + this.state + '.' + name)
133- debug(params)
134- }
135- + this.emit('writePacket', name, params)
136- this.serializer.write({ name, params })
137- }
138-
0 commit comments