Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PrismarineJS
GitHub Repository: PrismarineJS/mineflayer
Path: blob/master/test/internalTest.js
9427 views
1
/* eslint-env mocha */
2
3
const mineflayer = require('../')
4
const vec3 = require('vec3')
5
const mc = require('minecraft-protocol')
6
const assert = require('assert')
7
const { sleep } = require('../lib/promise_utils')
8
const nbt = require('prismarine-nbt')
9
const { once } = require('../lib/promise_utils')
10
const { getPort } = require('./common/util')
11
12
for (const supportedVersion of mineflayer.testedVersions) {
13
const registry = require('prismarine-registry')(supportedVersion)
14
const version = registry.version
15
const Chunk = require('prismarine-chunk')(supportedVersion)
16
17
const hasSignedChat = registry.supportFeature('signedChat')
18
function chatText (text) {
19
// TODO: move this to prismarine-chat in a new ChatMessage(text).toNotch(asNbt) method
20
return registry.supportFeature('chatPacketsUseNbtComponents')
21
? nbt.comp({ text: nbt.string(text) })
22
: JSON.stringify({ text })
23
}
24
25
function generateChunkPacket (chunk) {
26
const lights = chunk.dumpLight()
27
return {
28
x: 0,
29
z: 0,
30
groundUp: true,
31
biomes: chunk.dumpBiomes !== undefined ? chunk.dumpBiomes() : undefined,
32
heightmaps: {
33
type: 'compound',
34
name: '',
35
value: {
36
MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([0, 0]) }
37
}
38
}, // send fake heightmap
39
bitMap: chunk.getMask(),
40
chunkData: chunk.dump(),
41
blockEntities: [],
42
trustEdges: false,
43
skyLightMask: lights?.skyLightMask,
44
blockLightMask: lights?.blockLightMask,
45
emptySkyLightMask: lights?.emptySkyLightMask,
46
emptyBlockLightMask: lights?.emptyBlockLightMask,
47
skyLight: lights?.skyLight,
48
blockLight: lights?.blockLight
49
}
50
}
51
52
describe(`mineflayer_internal ${supportedVersion}v`, function () {
53
this.timeout(10 * 1000)
54
let bot
55
let server
56
let PORT
57
beforeEach(async function () {
58
PORT = await getPort()
59
server = mc.createServer({
60
'online-mode': false,
61
version: supportedVersion,
62
port: PORT
63
})
64
await once(server, 'listening')
65
bot = mineflayer.createBot({
66
username: 'player',
67
version: supportedVersion,
68
port: PORT
69
})
70
bot.test = {}
71
72
bot.test.buildChunk = () => {
73
if (bot.supportFeature('tallWorld')) {
74
return new Chunk({ minY: -64, worldHeight: 384 })
75
} else {
76
return new Chunk()
77
}
78
}
79
80
bot.test.generateLoginPacket = () => {
81
let loginPacket
82
if (bot.supportFeature('usesLoginPacket')) {
83
loginPacket = registry.loginPacket
84
loginPacket.entityId = 0 // Default login packet in minecraft-data 1.16.5 is 1, so set it to 0
85
} else {
86
loginPacket = {
87
entityId: 0,
88
levelType: 'fogetaboutit',
89
gameMode: 0,
90
previousGameMode: 255,
91
worldNames: ['minecraft:overworld'],
92
dimension: 0,
93
worldName: 'minecraft:overworld',
94
hashedSeed: [0, 0],
95
difficulty: 0,
96
maxPlayers: 20,
97
reducedDebugInfo: 1,
98
enableRespawnScreen: true
99
}
100
}
101
return loginPacket
102
}
103
})
104
afterEach((done) => {
105
bot.on('end', () => {
106
done()
107
})
108
server.close()
109
})
110
it('chat', (done) => {
111
bot.once('chat', (username, message) => {
112
assert.strictEqual(username, 'gary')
113
assert.strictEqual(message, 'hello')
114
bot.chat('hi')
115
})
116
server.on('playerJoin', (client) => {
117
client.write('login', bot.test.generateLoginPacket())
118
const message = hasSignedChat
119
? JSON.stringify({ text: 'hello' })
120
: JSON.stringify({
121
translate: 'chat.type.text',
122
with: [{
123
text: 'gary'
124
},
125
'hello'
126
]
127
})
128
129
if (hasSignedChat) {
130
const uuid = 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43' // random
131
const networkName = chatText('gary')
132
133
if (registry.supportFeature('incrementedChatType')) {
134
client.write('player_chat', {
135
plainMessage: 'hello',
136
filterType: 0,
137
type: { chatType: 0 },
138
networkName,
139
previousMessages: [],
140
senderUuid: uuid,
141
timestamp: Date.now(),
142
index: 0,
143
salt: 1n
144
})
145
} else if (registry.supportFeature('useChatSessions')) {
146
client.write('player_chat', {
147
plainMessage: 'hello',
148
filterType: 0,
149
type: { chatType: 0 },
150
networkName,
151
previousMessages: [],
152
senderUuid: uuid,
153
timestamp: Date.now(),
154
index: 0,
155
salt: 2n
156
})
157
} else if (registry.supportFeature('chainedChatWithHashing')) {
158
client.write('player_chat', {
159
plainMessage: 'hello',
160
filterType: 0,
161
type: 0,
162
networkName,
163
previousMessages: [],
164
senderUuid: uuid,
165
timestamp: Date.now(),
166
salt: 3n,
167
signature: Buffer.alloc(0)
168
})
169
} else {
170
client.write('player_chat', {
171
signedChatContent: '',
172
unsignedChatContent: message,
173
type: 0,
174
senderUuid: uuid,
175
senderName: JSON.stringify({ text: 'gary' }),
176
senderTeam: undefined,
177
timestamp: Date.now(),
178
salt: 4n,
179
signature: Buffer.alloc(0)
180
})
181
}
182
} else {
183
client.write('chat', { message, position: 0, sender: '0' })
184
}
185
function onChat (packet) {
186
const msg = packet.message || packet.unsignedChatContent || packet.signedChatContent
187
assert.strictEqual(msg, 'hi')
188
done()
189
}
190
client.on('chat_message', onChat)
191
client.on('chat', onChat)
192
})
193
})
194
it('entity effects', (done) => {
195
bot.once('entityEffect', (entity, effect) => {
196
assert.strictEqual(entity.id, 8)
197
assert.strictEqual(effect.id, 10)
198
assert.strictEqual(effect.amplifier, 1)
199
assert.strictEqual(effect.duration, 11)
200
done()
201
})
202
// Versions prior to 1.11 have capital first letter
203
const entities = bot.registry.entitiesByName
204
const creeperId = entities.creeper ? entities.creeper.id : entities.Creeper.id
205
server.on('playerJoin', (client) => {
206
client.write(bot.registry.supportFeature('consolidatedEntitySpawnPacket') ? 'spawn_entity' : 'spawn_entity_living', {
207
entityId: 8, // random
208
entityUUID: '00112233-4455-6677-8899-aabbccddeeff',
209
objectUUID: '00112233-4455-6677-8899-aabbccddeeff',
210
type: creeperId,
211
x: 10,
212
y: 11,
213
z: 12,
214
yaw: 13,
215
pitch: 14,
216
headPitch: 14,
217
velocity: { x: 15, y: 16, z: 17 },
218
velocityX: 16,
219
velocityY: 17,
220
velocityZ: 18,
221
metadata: []
222
})
223
client.write('entity_effect', {
224
entityId: 8,
225
effectId: 10,
226
amplifier: 1,
227
duration: 11,
228
hideParticles: false
229
})
230
})
231
})
232
it('blockAt', (done) => {
233
const pos = vec3(1, 65, 1)
234
const goldId = bot.registry.blocksByName.gold_block.id
235
bot.on('chunkColumnLoad', (columnPoint) => {
236
assert.strictEqual(columnPoint.x, 0)
237
assert.strictEqual(columnPoint.z, 0)
238
assert.strictEqual(bot.blockAt(pos).type, goldId)
239
done()
240
})
241
server.on('playerJoin', (client) => {
242
client.write('login', bot.test.generateLoginPacket())
243
const chunk = bot.test.buildChunk()
244
chunk.setBlockType(pos, goldId)
245
client.write('map_chunk', generateChunkPacket(chunk))
246
})
247
})
248
249
describe('digTime', () => {
250
it('should use eye-level water check instead of isInWater for dig speed', (done) => {
251
const blockPos = vec3(1, 65, 1)
252
const playerPos = vec3(1.5, 66, 1.5)
253
// eyeHeight is 1.62, so eye level at y=67.62 -> block at y=67
254
// A second position where eye level has water: player at y=70, eye at y=71.62 -> block y=71
255
const playerPos2 = vec3(1.5, 70, 1.5)
256
const eyeLevelBlockPos2 = vec3(1, 71, 1)
257
const dirtId = bot.registry.blocksByName.dirt.id
258
const waterId = bot.registry.blocksByName.water.id
259
const blockPos2 = vec3(1, 69, 1)
260
261
bot.on('chunkColumnLoad', () => {
262
// Set bot entity properties
263
bot.entity.eyeHeight = 1.62
264
bot.entity.onGround = true
265
bot.entity.isInWater = false
266
bot.entity.effects = {}
267
bot.game = bot.game || {}
268
bot.game.gameMode = 'survival'
269
270
// Test 1: No water at eye level -> normal dig speed
271
bot.entity.position = playerPos
272
const block1 = bot.blockAt(blockPos)
273
const digTimeNoWater = bot.digTime(block1)
274
275
// Test 2: Water at eye level -> slower dig speed
276
bot.entity.position = playerPos2
277
const block2 = bot.blockAt(blockPos2)
278
const digTimeWithWater = bot.digTime(block2)
279
280
// Digging in water should be slower (higher dig time)
281
assert(digTimeWithWater > digTimeNoWater,
282
`Dig time with water at eye level (${digTimeWithWater}) should be greater than without (${digTimeNoWater})`)
283
284
// Test 3: isInWater=true but no water block at eye level should NOT slow digging
285
// (this is the bug that was fixed - previously isInWater incorrectly affected dig speed)
286
bot.entity.position = playerPos
287
bot.entity.isInWater = true
288
const digTimeFeetInWater = bot.digTime(block1)
289
assert.strictEqual(digTimeFeetInWater, digTimeNoWater,
290
'isInWater should not affect dig time when eye level is not in water')
291
292
done()
293
})
294
server.on('playerJoin', (client) => {
295
client.write('login', bot.test.generateLoginPacket())
296
const chunk = bot.test.buildChunk()
297
// Place dirt blocks to dig at two locations
298
chunk.setBlockType(blockPos, dirtId)
299
chunk.setBlockType(blockPos2, dirtId)
300
// Place water only at the second eye-level position
301
chunk.setBlockType(eyeLevelBlockPos2, waterId)
302
client.write('map_chunk', generateChunkPacket(chunk))
303
})
304
})
305
})
306
307
describe('physics', () => {
308
const pos = vec3(1, 65, 1)
309
const goldId = 41
310
it('no physics if there is no chunk', (done) => {
311
let fail = 0
312
const basePosition = {
313
x: 1.5,
314
y: 66,
315
z: 1.5,
316
dx: 0, // 1.21.3
317
dy: 0, // 1.21.3
318
dz: 0, // 1.21.3
319
pitch: 0,
320
yaw: 0,
321
flags: bot.registry.version['>=']('1.21.3') ? {} : 0,
322
teleportId: 0
323
}
324
server.on('playerJoin', async (client) => {
325
await client.write('login', bot.test.generateLoginPacket())
326
await client.write('position', basePosition)
327
client.on('packet', (data, meta) => {
328
const packetName = meta.name
329
switch (packetName) {
330
case 'position':
331
fail++
332
break
333
case 'position_look':
334
fail++
335
break
336
case 'look':
337
fail++
338
break
339
}
340
if (fail > 1) assert.fail('position packet sent')
341
})
342
await sleep(2000)
343
done()
344
})
345
})
346
it('absolute position & relative position (velocity)', (done) => {
347
server.on('playerJoin', async (client) => {
348
await client.write('login', bot.test.generateLoginPacket())
349
const chunk = bot.test.buildChunk()
350
chunk.setBlockType(pos, goldId)
351
await client.write('map_chunk', generateChunkPacket(chunk))
352
353
await once(bot, 'chunkColumnLoad')
354
355
// --- Test 1: Absolute Position ---
356
const absolutePositionPacket = {
357
x: 1.5,
358
y: 80,
359
z: 1.5,
360
pitch: 0,
361
yaw: 0,
362
teleportId: 1,
363
flags: bot.supportFeature('positionPacketHasBitflags') ? { x: false, y: false, z: false, yaw: false, pitch: false } : 0
364
}
365
366
bot.entity.velocity.y = -1.0 // Give bot some velocity
367
368
const p1 = once(bot, 'forcedMove')
369
client.write('position', absolutePositionPacket)
370
await p1
371
372
// Assertions for absolute teleport
373
assert.strictEqual(bot.entity.velocity.y, 0, 'Velocity should be reset to 0 after an absolute teleport')
374
assert.deepStrictEqual(bot.entity.position, vec3(1.5, 80, 1.5), 'Position should be set absolutely')
375
376
// --- Test 2: Relative Position ---
377
const relativePositionPacket = {
378
x: 1.0,
379
y: -2.0,
380
z: 0.5,
381
pitch: 0,
382
yaw: 0,
383
teleportId: 2,
384
flags: bot.supportFeature('positionPacketHasBitflags') ? { x: true, y: true, z: true, yaw: false, pitch: false } : 7
385
}
386
387
// Set a known velocity *before* the relative update
388
bot.entity.velocity.y = -1.0
389
const initialPosition = bot.entity.position.clone()
390
const expectedPosition = initialPosition.plus(vec3(1.0, -2.0, 0.5))
391
392
const p2 = once(bot, 'forcedMove')
393
client.write('position', relativePositionPacket)
394
await p2
395
396
// Assertions for relative teleport
397
assert.notStrictEqual(bot.entity.velocity.y, 0, 'Velocity should be preserved after a relative teleport')
398
assert.deepStrictEqual(bot.entity.position, expectedPosition, 'Position should be updated relatively')
399
400
done()
401
})
402
})
403
it('gravity + land on solid block + jump', (done) => {
404
let y = 80
405
let landed = false
406
bot.on('move', () => {
407
if (landed) return
408
assert.ok(bot.entity.position.y <= y)
409
assert.ok(bot.entity.position.y >= pos.y)
410
y = bot.entity.position.y
411
if (bot.entity.position.y <= pos.y + 1) {
412
assert.strictEqual(bot.entity.position.y, pos.y + 1)
413
assert.strictEqual(bot.entity.onGround, true)
414
landed = true
415
done()
416
} else {
417
assert.strictEqual(bot.entity.onGround, false)
418
}
419
})
420
server.on('playerJoin', (client) => {
421
client.write('login', bot.test.generateLoginPacket())
422
const chunk = bot.test.buildChunk()
423
424
chunk.setBlockType(pos, goldId)
425
client.write('map_chunk', generateChunkPacket(chunk))
426
client.write('position', {
427
x: 1.5,
428
y: 80,
429
z: 1.5,
430
pitch: 0,
431
yaw: 0,
432
flags: bot.supportFeature('positionPacketHasBitflags') ? { x: false, y: false, z: false, yaw: false, pitch: false } : 0,
433
teleportId: 0
434
})
435
})
436
})
437
})
438
439
describe('world', () => {
440
const pos = vec3(1, 65, 1)
441
const goldId = 41
442
it('switchWorld respawn', (done) => {
443
const loginPacket = bot.test.generateLoginPacket()
444
let respawnPacket
445
if (bot.supportFeature('usesLoginPacket')) {
446
loginPacket.worldName = 'minecraft:overworld'
447
loginPacket.hashedSeed = [0, 0]
448
loginPacket.entityId = 0
449
respawnPacket = {
450
// 1.19+ the `dimension` filed is a string in respawn packet and undefined in login packet, in previous versions it's same NBT data in login/respawn
451
dimension: bot.supportFeature('dimensionDataInCodec') ? 'minecraft:overworld' : loginPacket.dimension,
452
worldName: loginPacket.worldName,
453
hashedSeed: loginPacket.hashedSeed,
454
gamemode: 0,
455
previousGamemode: 255,
456
isDebug: false,
457
isFlat: false,
458
copyMetadata: true,
459
death: {
460
dimensionName: '',
461
location: {
462
x: 0,
463
y: 0,
464
z: 0
465
}
466
}
467
}
468
if (bot.supportFeature('spawnRespawnWorldDataField')) {
469
respawnPacket = {
470
worldState: respawnPacket
471
}
472
respawnPacket.worldState.name = loginPacket.worldName
473
respawnPacket.worldState.dimension = loginPacket.dimension
474
}
475
} else {
476
respawnPacket = {
477
dimension: 0,
478
hashedSeed: [0, 0],
479
gamemode: 0,
480
levelType: 'default'
481
}
482
}
483
const chunk = bot.test.buildChunk()
484
chunk.setBlockType(pos, goldId)
485
const chunkPacket = generateChunkPacket(chunk)
486
const positionPacket = {
487
x: 1.5,
488
y: 80,
489
z: 1.5,
490
pitch: 0,
491
yaw: 0,
492
flags: 0,
493
teleportId: 0
494
}
495
server.on('playerJoin', async (client) => {
496
bot.once('respawn', () => {
497
assert.ok(bot.world.getColumn(0, 0) !== undefined)
498
bot.once('respawn', () => {
499
assert.ok(bot.world.getColumn(0, 0) === undefined)
500
done()
501
})
502
if (bot.supportFeature('spawnRespawnWorldDataField')) {
503
respawnPacket.worldState.name = 'minecraft:nether'
504
} else {
505
respawnPacket.worldName = 'minecraft:nether'
506
}
507
if (bot.supportFeature('spawnRespawnWorldDataField')) {
508
respawnPacket.worldState.dimension = 1
509
} else if (bot.supportFeature('usesLoginPacket')) {
510
respawnPacket.dimension.name = 'e'
511
} else {
512
respawnPacket.dimension = 1
513
}
514
client.write('respawn', respawnPacket)
515
})
516
await client.write('login', loginPacket)
517
await client.write('map_chunk', chunkPacket)
518
await client.write('position', positionPacket)
519
await client.write('update_health', {
520
health: 20,
521
food: 20,
522
foodSaturation: 0
523
})
524
await bot.waitForTicks(1)
525
await client.write('respawn', respawnPacket)
526
})
527
})
528
})
529
530
describe('game', () => {
531
it('responds to ping / transaction packets', (done) => { // only on 1.17
532
server.on('playerJoin', async (client) => {
533
if (bot.supportFeature('transactionPacketExists')) {
534
const transactionPacket = { windowId: 0, action: 42, accepted: false }
535
client.once('transaction', (data, meta) => {
536
assert.ok(meta.name === 'transaction')
537
assert.ok(data.action === 42)
538
assert.ok(data.accepted === true)
539
done()
540
})
541
client.write('transaction', transactionPacket)
542
} else {
543
client.once('pong', (data) => {
544
assert(data.id === 42)
545
done()
546
})
547
client.write('ping', { id: 42 })
548
}
549
})
550
})
551
552
it('dimension type lookup uses worldType over worldName on 1.19-1.20.4', function (done) {
553
// On proxy/modded servers the worldName (level name) may differ from
554
// the dimension type. For versions with dimensionDataInCodec but
555
// without segmentedRegistryCodecData (1.19-1.20.4), the bot should
556
// prefer worldType (login) / dimension (respawn) for the codec lookup.
557
if (!bot.supportFeature('dimensionDataInCodec') || bot.supportFeature('segmentedRegistryCodecData')) {
558
this.skip()
559
return
560
}
561
562
const loginPacket = bot.test.generateLoginPacket()
563
// Simulate a proxy/modded server: worldName is a custom level name
564
// but worldType is still the real dimension type
565
loginPacket.worldName = 'modded:custom_world'
566
loginPacket.worldType = 'minecraft:overworld'
567
568
server.on('playerJoin', (client) => {
569
client.write('login', loginPacket)
570
bot.once('login', () => {
571
assert.strictEqual(bot.game.dimension, 'overworld',
572
'should use worldType for dimension, not worldName')
573
assert.ok(bot.game.minY !== undefined,
574
'minY should be set from codec lookup')
575
assert.ok(bot.game.height !== undefined,
576
'height should be set from codec lookup')
577
done()
578
})
579
})
580
})
581
})
582
583
describe('entities', () => {
584
it('entity id changes on login', (done) => {
585
const loginPacket = bot.test.generateLoginPacket()
586
server.on('playerJoin', (client) => {
587
if (bot.supportFeature('usesLoginPacket')) {
588
loginPacket.entityId = 0 // Default login packet in minecraft-data 1.16.5 is 1, so set it to 0
589
}
590
client.write('login', loginPacket)
591
bot.once('login', () => {
592
assert.ok(bot.entity.id === 0)
593
loginPacket.entityId = 42
594
bot.once('login', () => {
595
assert.ok(bot.entity.id === 42)
596
done()
597
})
598
client.write('login', loginPacket)
599
})
600
})
601
})
602
603
it('player displayName', (done) => {
604
server.on('playerJoin', (client) => {
605
bot.on('entitySpawn', (entity) => {
606
const player = bot.players[entity.username]
607
assert.strictEqual(entity.username, player.displayName.toString())
608
if (registry.supportFeature('playerInfoActionIsBitfield')) {
609
client.write('player_info', {
610
action: { update_display_name: true },
611
data: [{
612
uuid: '1-2-3-4',
613
displayName: chatText('wvffle')
614
}]
615
})
616
} else {
617
client.write('player_info', {
618
action: 'update_display_name',
619
data: [{
620
uuid: '1-2-3-4',
621
displayName: chatText('wvffle')
622
}]
623
})
624
}
625
})
626
627
bot.once('playerUpdated', (player) => {
628
assert.strictEqual('wvffle', player.displayName.toString())
629
if (registry.supportFeature('playerInfoActionIsBitfield')) {
630
client.write('player_info', {
631
action: { update_display_name: true },
632
data: [{
633
uuid: '1-2-3-4',
634
displayName: null
635
}]
636
})
637
} else {
638
client.write('player_info', {
639
action: 'update_display_name',
640
data: [{
641
uuid: '1-2-3-4',
642
displayName: null
643
}]
644
})
645
}
646
647
bot.once('playerUpdated', (player) => {
648
assert.strictEqual(player.entity.username, player.displayName.toString())
649
done()
650
})
651
})
652
653
if (registry.supportFeature('playerInfoActionIsBitfield')) {
654
client.write('player_info', {
655
action: { add_player: true },
656
data: [{
657
uuid: '1-2-3-4',
658
player: {
659
name: 'bot5',
660
properties: []
661
},
662
gamemode: 0,
663
latency: 0
664
}]
665
})
666
} else {
667
client.write('player_info', {
668
action: 'add_player',
669
data: [{
670
uuid: '1-2-3-4',
671
name: 'bot5',
672
properties: [],
673
gamemode: 0,
674
ping: 0
675
}]
676
})
677
}
678
679
if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
680
client.write('spawn_entity', {
681
entityId: 56,
682
objectUUID: '1-2-3-4',
683
type: bot.registry.entitiesByName.player.internalId,
684
x: 1,
685
y: 2,
686
z: 3,
687
pitch: 0,
688
yaw: 0,
689
headPitch: 0,
690
objectData: 1,
691
velocity: { x: 0, y: 0, z: 0 },
692
velocityX: 0,
693
velocityY: 0,
694
velocityZ: 0
695
})
696
} else {
697
client.write('named_entity_spawn', {
698
entityId: 56,
699
playerUUID: '1-2-3-4',
700
x: 1,
701
y: 2,
702
z: 3,
703
yaw: 0,
704
pitch: 0,
705
currentItem: -1,
706
metadata: []
707
})
708
}
709
})
710
})
711
712
it('does not crash when skin texture is mojangson format', (done) => {
713
// Mojangson-style texture data (not valid JSON, but valid mojangson)
714
const mojangsonStr = '{textures:{SKIN:{url:"http://textures.minecraft.net/texture/abc123",metadata:{model:"slim"}}}}'
715
const mojangsonBase64 = Buffer.from(mojangsonStr).toString('base64')
716
717
server.on('playerJoin', (client) => {
718
bot.on('entitySpawn', (entity) => {
719
const player = bot.players[entity.username]
720
assert.ok(player, 'player should exist')
721
assert.ok(player.skinData, 'skinData should be parsed from mojangson')
722
assert.strictEqual(player.skinData.url, 'http://textures.minecraft.net/texture/abc123')
723
assert.strictEqual(player.skinData.model, 'slim')
724
done()
725
})
726
727
if (registry.supportFeature('playerInfoActionIsBitfield')) {
728
client.write('player_info', {
729
action: { add_player: true },
730
data: [{
731
uuid: '1-2-3-4',
732
player: {
733
name: 'bot5',
734
properties: [{
735
name: 'textures',
736
value: mojangsonBase64,
737
signature: ''
738
}]
739
},
740
gamemode: 0,
741
latency: 0
742
}]
743
})
744
} else {
745
client.write('player_info', {
746
action: 'add_player',
747
data: [{
748
uuid: '1-2-3-4',
749
name: 'bot5',
750
properties: [{
751
name: 'textures',
752
value: mojangsonBase64,
753
signature: ''
754
}],
755
gamemode: 0,
756
ping: 0
757
}]
758
})
759
}
760
761
if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
762
client.write('spawn_entity', {
763
entityId: 56,
764
objectUUID: '1-2-3-4',
765
type: bot.registry.entitiesByName.player.internalId,
766
x: 1,
767
y: 2,
768
z: 3,
769
pitch: 0,
770
yaw: 0,
771
headPitch: 0,
772
objectData: 1,
773
velocity: { x: 0, y: 0, z: 0 },
774
velocityX: 0,
775
velocityY: 0,
776
velocityZ: 0
777
})
778
} else {
779
client.write('named_entity_spawn', {
780
entityId: 56,
781
playerUUID: '1-2-3-4',
782
x: 1,
783
y: 2,
784
z: 3,
785
yaw: 0,
786
pitch: 0,
787
currentItem: -1,
788
metadata: []
789
})
790
}
791
})
792
})
793
794
it('does not crash when skin texture is valid JSON', (done) => {
795
const validJson = JSON.stringify({
796
textures: {
797
SKIN: {
798
url: 'http://textures.minecraft.net/texture/def456',
799
metadata: { model: 'default' }
800
}
801
}
802
})
803
const jsonBase64 = Buffer.from(validJson).toString('base64')
804
805
server.on('playerJoin', (client) => {
806
bot.on('entitySpawn', (entity) => {
807
const player = bot.players[entity.username]
808
assert.ok(player, 'player should exist')
809
assert.ok(player.skinData, 'skinData should be parsed from JSON')
810
assert.strictEqual(player.skinData.url, 'http://textures.minecraft.net/texture/def456')
811
assert.strictEqual(player.skinData.model, 'default')
812
done()
813
})
814
815
if (registry.supportFeature('playerInfoActionIsBitfield')) {
816
client.write('player_info', {
817
action: { add_player: true },
818
data: [{
819
uuid: '1-2-3-4',
820
player: {
821
name: 'bot6',
822
properties: [{
823
name: 'textures',
824
value: jsonBase64,
825
signature: ''
826
}]
827
},
828
gamemode: 0,
829
latency: 0
830
}]
831
})
832
} else {
833
client.write('player_info', {
834
action: 'add_player',
835
data: [{
836
uuid: '1-2-3-4',
837
name: 'bot6',
838
properties: [{
839
name: 'textures',
840
value: jsonBase64,
841
signature: ''
842
}],
843
gamemode: 0,
844
ping: 0
845
}]
846
})
847
}
848
849
if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
850
client.write('spawn_entity', {
851
entityId: 57,
852
objectUUID: '1-2-3-4',
853
type: bot.registry.entitiesByName.player.internalId,
854
x: 1,
855
y: 2,
856
z: 3,
857
pitch: 0,
858
yaw: 0,
859
headPitch: 0,
860
objectData: 1,
861
velocity: { x: 0, y: 0, z: 0 },
862
velocityX: 0,
863
velocityY: 0,
864
velocityZ: 0
865
})
866
} else {
867
client.write('named_entity_spawn', {
868
entityId: 57,
869
playerUUID: '1-2-3-4',
870
x: 1,
871
y: 2,
872
z: 3,
873
yaw: 0,
874
pitch: 0,
875
currentItem: -1,
876
metadata: []
877
})
878
}
879
})
880
})
881
882
it('sets players[player].entity to null upon despawn', (done) => {
883
let serverClient = null
884
bot.once('entitySpawn', (entity) => {
885
if (bot.version !== '1.17') {
886
serverClient.write('entity_destroy', {
887
entityIds: [8]
888
})
889
} else {
890
serverClient.write('destroy_entity', {
891
entityIds: 8
892
})
893
}
894
})
895
bot.once('entityGone', (entity) => {
896
assert.strictEqual(bot.players[entity.username], undefined)
897
done()
898
})
899
server.on('playerJoin', (client) => {
900
serverClient = client
901
902
if (registry.supportFeature('playerInfoActionIsBitfield')) {
903
client.write('player_info', {
904
action: { add_player: true },
905
data: [{
906
uuid: '1-2-3-4',
907
player: { name: 'bot5', properties: [] },
908
gamemode: 0,
909
latency: 0
910
}]
911
})
912
} else {
913
client.write('player_info', {
914
id: 56,
915
state: 'play',
916
action: 'add_player',
917
length: 1,
918
data: [{
919
uuid: '1-2-3-4',
920
name: 'bot5',
921
propertiesLength: 0,
922
properties: [],
923
gamemode: 0,
924
ping: 0,
925
hasDisplayName: false
926
}]
927
})
928
}
929
930
if (bot.registry.supportFeature('unifiedPlayerAndEntitySpawnPacket')) {
931
client.write('spawn_entity', {
932
entityId: 56,
933
objectUUID: '1-2-3-4',
934
type: bot.registry.entitiesByName.player.internalId,
935
x: 1,
936
y: 2,
937
z: 3,
938
pitch: 0,
939
yaw: 0,
940
headPitch: 0,
941
objectData: 1,
942
velocity: { x: 0, y: 0, z: 0 },
943
velocityX: 0,
944
velocityY: 0,
945
velocityZ: 0
946
})
947
} else {
948
client.write('named_entity_spawn', {
949
entityId: 56,
950
playerUUID: '1-2-3-4',
951
x: 1,
952
y: 2,
953
z: 3,
954
yaw: 0,
955
pitch: 0,
956
currentItem: -1,
957
metadata: []
958
})
959
}
960
})
961
})
962
963
it('metadata', (done) => {
964
server.on('playerJoin', (client) => {
965
bot.on('entitySpawn', (entity) => {
966
assert.strictEqual(entity.displayName, 'Creeper')
967
968
const lastMeta = entity.metadata
969
bot.on('entityUpdate', (entity) => {
970
assert.ok('0' in entity.metadata)
971
assert.strictEqual(entity.metadata[0], 1)
972
assert.strictEqual(entity.metadata[1], lastMeta[1])
973
done()
974
})
975
976
client.write('entity_metadata', {
977
entityId: 8,
978
metadata: [
979
{ key: 0, type: bot.registry.supportFeature('mcDataHasEntityMetadata') ? 'int' : 0, value: 1 }
980
]
981
})
982
})
983
984
// Versions prior to 1.11 have capital first letter
985
const entities = bot.registry.entitiesByName
986
const creeperId = entities.creeper ? entities.creeper.id : entities.Creeper.id
987
client.write(bot.registry.supportFeature('consolidatedEntitySpawnPacket') ? 'spawn_entity' : 'spawn_entity_living', {
988
entityId: 8, // random
989
entityUUID: '00112233-4455-6677-8899-aabbccddeeff',
990
objectUUID: '00112233-4455-6677-8899-aabbccddeeff',
991
type: creeperId,
992
x: 10,
993
y: 11,
994
z: 12,
995
yaw: 13,
996
pitch: 14,
997
headPitch: 14,
998
velocity: { x: 15, y: 16, z: 17 },
999
velocityX: 16,
1000
velocityY: 17,
1001
velocityZ: 18,
1002
metadata: [
1003
{ type: 0, key: bot.registry.supportFeature('mcDataHasEntityMetadata') ? 'byte' : 0, value: 0 },
1004
{ type: 0, key: bot.registry.supportFeature('mcDataHasEntityMetadata') ? 'int' : 1, value: 1 }
1005
]
1006
})
1007
})
1008
})
1009
1010
it('\'itemDrop\' event', function (done) {
1011
const itemData = {
1012
itemId: 149,
1013
itemCount: 5
1014
}
1015
1016
server.on('playerJoin', (client) => {
1017
bot.on('itemDrop', (entity) => {
1018
const slotPosition = metadataPacket.metadata[0].key
1019
1020
if (bot.supportFeature('itemsAreAlsoBlocks')) {
1021
assert.strictEqual(entity.metadata[slotPosition].blockId, itemData.itemId)
1022
} else if (bot.supportFeature('itemsAreNotBlocks')) {
1023
assert.strictEqual(entity.metadata[slotPosition].itemId, itemData.itemId)
1024
}
1025
assert.strictEqual(entity.metadata[slotPosition].itemCount, itemData.itemCount)
1026
1027
done()
1028
})
1029
1030
let entityType
1031
if (['1.8', '1.9', '1.10', '1.11', '1.12'].includes(bot.majorVersion)) {
1032
entityType = 2
1033
} else {
1034
entityType = bot.registry.entitiesArray.find(e => e.name.toLowerCase() === 'item' || e.name.toLowerCase() === 'item_stack').id
1035
}
1036
client.write('spawn_entity', {
1037
entityId: 16,
1038
objectUUID: '00112233-4455-6677-8899-aabbccddeeff',
1039
type: Number(entityType),
1040
x: 0,
1041
y: 0,
1042
z: 0,
1043
pitch: 0,
1044
yaw: 0,
1045
headPitch: 0,
1046
objectData: 1,
1047
velocity: { x: 0, y: 0, z: 0 },
1048
velocityX: 0,
1049
velocityY: 0,
1050
velocityZ: 0
1051
})
1052
1053
const metadataPacket = {
1054
entityId: 16,
1055
metadata: [
1056
{ key: 7, type: 6, value: { itemCount: itemData.itemCount } }
1057
]
1058
}
1059
// Versions prior to 1.13 use 5 as type field value of metadata for storing a slot. 1.13 and so on, use 6
1060
// Also the structure of a slot changes from 1.12 to 1.13
1061
if (bot.supportFeature('itemsAreAlsoBlocks')) {
1062
metadataPacket.metadata[0].key = 6
1063
metadataPacket.metadata[0].type = 5
1064
metadataPacket.metadata[0].value.blockId = itemData.itemId
1065
metadataPacket.metadata[0].value.itemDamage = 0
1066
} else if (bot.supportFeature('itemsAreNotBlocks')) {
1067
if (bot.majorVersion === '1.13') metadataPacket.metadata[0].key = 6
1068
metadataPacket.metadata[0].value.itemId = itemData.itemId
1069
metadataPacket.metadata[0].value.present = true
1070
}
1071
1072
if (bot.supportFeature('entityMetadataHasLong')) {
1073
metadataPacket.metadata[0].type = 7
1074
}
1075
1076
if (bot.registry.supportFeature('mcDataHasEntityMetadata')) {
1077
metadataPacket.metadata[0].type = 'item_stack'
1078
}
1079
metadataPacket.metadata[0].value.addedComponentCount = 0
1080
metadataPacket.metadata[0].value.removedComponentCount = 0
1081
metadataPacket.metadata[0].value.components = []
1082
metadataPacket.metadata[0].value.removeComponents = []
1083
1084
client.write('entity_metadata', metadataPacket)
1085
})
1086
})
1087
})
1088
1089
it('bed', (done) => {
1090
const blocks = bot.registry.blocksByName
1091
const entities = bot.registry.entitiesByName
1092
1093
const playerPos = vec3(10, 0, 0)
1094
const zombiePos = vec3(0, 0, 0)
1095
const beds = [
1096
{ head: vec3(10, 0, 3), foot: vec3(10, 0, 2), facing: 2, throws: false },
1097
{ head: vec3(9, 0, 4), foot: vec3(10, 0, 4), facing: 3, throws: true, error: new Error('the bed is too far') },
1098
{ head: vec3(8, 0, 0), foot: vec3(8, 0, 1), facing: 0, throws: true, error: new Error('there are monsters nearby') },
1099
{ head: vec3(12, 0, 0), foot: vec3(11, 0, 0), facing: 1, throws: false }
1100
]
1101
1102
const zombieId = entities.zombie ? entities.zombie.id : entities.Zombie.id
1103
let bedBlock
1104
if (bot.supportFeature('oneBlockForSeveralVariations', version.majorVersion)) {
1105
bedBlock = blocks.bed
1106
} else if (bot.supportFeature('blockSchemeIsFlat', version.majorVersion)) {
1107
bedBlock = blocks.red_bed
1108
}
1109
const bedId = bedBlock.id
1110
1111
bot.once('chunkColumnLoad', (columnPoint) => {
1112
for (const bed in beds) {
1113
const bedBock = bot.blockAt(beds[bed].foot)
1114
const bedBockMetadata = bot.parseBedMetadata(bedBock)
1115
assert.strictEqual(bedBockMetadata.facing, beds[bed].facing, 'The facing property seems to be wrong')
1116
assert.strictEqual(bedBockMetadata.part, false, 'The part property seems to be wrong') // Is the foot
1117
1118
if (beds[bed].throws) {
1119
bot.sleep(bedBock).catch(err => assert.strictEqual(err, beds[bed].error))
1120
} else {
1121
bot.sleep(bedBock).catch(err => assert.ifError(err))
1122
}
1123
}
1124
1125
done()
1126
})
1127
1128
server.once('playerJoin', (client) => {
1129
const loginPacket = bot.test.generateLoginPacket()
1130
client.write('login', loginPacket)
1131
// Set timeOfDay after login is processed so bot.time is initialized
1132
bot.once('login', () => { bot.time.timeOfDay = 18000 })
1133
1134
const chunk = bot.test.buildChunk()
1135
1136
for (const bed in beds) {
1137
chunk.setBlockType(beds[bed].head, bedId)
1138
chunk.setBlockType(beds[bed].foot, bedId)
1139
}
1140
1141
if (bot.supportFeature('blockStateId', version.majorVersion)) {
1142
chunk.setBlockStateId(beds[0].foot, 3 + bedBlock.minStateId) // { facing: north, occupied: false, part: foot }
1143
chunk.setBlockStateId(beds[0].head, 2 + bedBlock.minStateId) // { facing:north, occupied: false, part: head }
1144
1145
chunk.setBlockStateId(beds[1].foot, 15 + bedBlock.minStateId) // { facing: east, occupied:false, part:foot }
1146
chunk.setBlockStateId(beds[1].head, 14 + bedBlock.minStateId) // { facing: east, occupied: false, part: head }
1147
1148
chunk.setBlockStateId(beds[2].foot, 7 + bedBlock.minStateId) // { facing: south, occupied: false, part: foot }
1149
chunk.setBlockStateId(beds[2].head, 6 + bedBlock.minStateId) // { facing: south, occupied: false, part: head }
1150
1151
chunk.setBlockStateId(beds[3].foot, 11 + bedBlock.minStateId) // { facing: west, occupied: false, part: foot }
1152
chunk.setBlockStateId(beds[3].head, 10 + bedBlock.minStateId) // { facing: west, occupied: false, part: head }
1153
} else if (bot.supportFeature('blockMetadata', version.majorVersion)) {
1154
chunk.setBlockData(beds[0].foot, 2) // { facing: north, occupied: false, part: foot }
1155
chunk.setBlockData(beds[0].head, 10) // { facing:north, occupied: false, part: head }
1156
1157
chunk.setBlockData(beds[1].foot, 3) // { facing: east, occupied:false, part:foot }
1158
chunk.setBlockData(beds[1].head, 11) // { facing: east, occupied: false, part: head }
1159
1160
chunk.setBlockData(beds[2].foot, 0) // { facing: south, occupied: false, part: foot }
1161
chunk.setBlockData(beds[2].head, 8) // { facing: south, occupied: false, part: head }
1162
1163
chunk.setBlockData(beds[3].foot, 1) // { facing: west, occupied: false, part: foot }
1164
chunk.setBlockData(beds[3].head, 9) // { facing: west, occupied: false, part: head }
1165
}
1166
1167
client.write('position', {
1168
x: playerPos.x,
1169
y: playerPos.y,
1170
z: playerPos.z,
1171
yaw: 0,
1172
pitch: 0,
1173
flags: 0,
1174
teleportId: 1
1175
})
1176
1177
client.write(bot.registry.supportFeature('consolidatedEntitySpawnPacket') ? 'spawn_entity' : 'spawn_entity_living', {
1178
entityId: 8,
1179
entityUUID: '00112233-4455-6677-8899-aabbccddeeff',
1180
objectUUID: '00112233-4455-6677-8899-aabbccddeeff',
1181
type: zombieId,
1182
x: zombiePos.x,
1183
y: zombiePos.y,
1184
z: zombiePos.z,
1185
yaw: 0,
1186
pitch: 0,
1187
headPitch: 0,
1188
velocity: { x: 0, y: 0, z: 0 },
1189
velocityX: 0,
1190
velocityY: 0,
1191
velocityZ: 0,
1192
metadata: []
1193
})
1194
1195
client.write('map_chunk', generateChunkPacket(chunk))
1196
})
1197
})
1198
1199
describe('heldItemChanged', () => {
1200
it('emits heldItemChanged when the held slot is updated via set_slot', (done) => {
1201
const Item = require('prismarine-item')(supportedVersion)
1202
const QUICK_BAR_SLOT = 0
1203
const HOTBAR_START = 36
1204
const stoneId = registry.itemsByName.stone.id
1205
const stoneItem = new Item(stoneId, 1)
1206
const notchItem = Item.toNotch(stoneItem)
1207
1208
server.on('playerJoin', (client) => {
1209
client.write('login', bot.test.generateLoginPacket())
1210
client.write('held_item_slot', { slot: QUICK_BAR_SLOT })
1211
1212
// Wait for the held_item_slot to be processed, then listen for the
1213
// heldItemChanged triggered by the set_slot update to the held slot
1214
setTimeout(() => {
1215
bot.once('heldItemChanged', (newItem) => {
1216
assert.ok(newItem, 'heldItemChanged should provide the new item')
1217
assert.strictEqual(newItem.type, stoneId)
1218
done()
1219
})
1220
client.write('set_slot', {
1221
windowId: 0,
1222
slot: HOTBAR_START + QUICK_BAR_SLOT,
1223
item: notchItem
1224
})
1225
}, 100)
1226
})
1227
})
1228
1229
it('emits heldItemChanged via updateSlot on the inventory', (done) => {
1230
const Item = require('prismarine-item')(supportedVersion)
1231
const QUICK_BAR_SLOT = 0
1232
const stoneId = registry.itemsByName.stone.id
1233
const stoneItem = new Item(stoneId, 1)
1234
1235
server.on('playerJoin', (client) => {
1236
client.write('login', bot.test.generateLoginPacket())
1237
client.write('held_item_slot', { slot: QUICK_BAR_SLOT })
1238
1239
setTimeout(() => {
1240
bot.once('heldItemChanged', (newItem) => {
1241
assert.ok(newItem, 'heldItemChanged should provide the new item')
1242
assert.strictEqual(newItem.type, stoneId)
1243
done()
1244
})
1245
// Directly call updateSlot on the inventory to simulate
1246
// the set_player_inventory code path
1247
bot.inventory.updateSlot(
1248
QUICK_BAR_SLOT + bot.inventory.hotbarStart,
1249
stoneItem
1250
)
1251
}, 100)
1252
})
1253
})
1254
})
1255
1256
describe('tablist', () => {
1257
it('handles newlines in header and footer', (done) => {
1258
const HEADER = 'asd\ndsa'
1259
const FOOTER = '\nas\nas\nas\n'
1260
bot._client.on('playerlist_header', (packet) => {
1261
setImmediate(() => {
1262
assert.strictEqual(bot.tablist.header.toString(), HEADER)
1263
assert.strictEqual(bot.tablist.footer.toString(), FOOTER)
1264
done()
1265
})
1266
})
1267
// TODO: figure out how the "extra" should be encoded in NBT so this branch can be removed
1268
if (registry.supportFeature('chatPacketsUseNbtComponents')) {
1269
server.on('playerJoin', (client) => {
1270
client.write('playerlist_header', {
1271
header: chatText(HEADER),
1272
footer: chatText(FOOTER)
1273
})
1274
})
1275
} else {
1276
server.on('playerJoin', (client) => {
1277
client.write('playerlist_header', {
1278
header: JSON.stringify({ text: '', extra: [{ text: HEADER, color: 'yellow' }] }),
1279
footer: JSON.stringify({ text: '', extra: [{ text: FOOTER, color: 'yellow' }] })
1280
})
1281
})
1282
}
1283
})
1284
})
1285
1286
describe('activateItem rotation', () => {
1287
it('should send the bot rotation in the use_item packet', function (done) {
1288
// The rotation field in use_item was added in 1.21.1
1289
const useItemFields = registry.protocol?.play?.toServer?.types?.packet_use_item?.[1]
1290
const hasRotation = useItemFields && useItemFields.some(f => f.name === 'rotation')
1291
if (!hasRotation) {
1292
this.skip()
1293
return
1294
}
1295
const { toNotchianYaw, toNotchianPitch } = require('../lib/conversions')
1296
const testYaw = 1.5
1297
const testPitch = -0.3
1298
server.on('playerJoin', async (client) => {
1299
await client.write('login', bot.test.generateLoginPacket())
1300
await client.write('position', {
1301
x: 0,
1302
y: 66,
1303
z: 0,
1304
dx: 0,
1305
dy: 0,
1306
dz: 0,
1307
yaw: 0,
1308
pitch: 0,
1309
flags: bot.registry.version['>=']('1.21.3') ? {} : 0,
1310
teleportId: 0
1311
})
1312
1313
client.on('packet', (data, meta) => {
1314
if (meta.name === 'use_item') {
1315
const expectedYaw = toNotchianYaw(testYaw)
1316
const expectedPitch = toNotchianPitch(testPitch)
1317
assert.ok(data.rotation, 'use_item packet should have rotation field')
1318
assert.ok(Math.abs(data.rotation.x - expectedYaw) < 0.001,
1319
`Expected yaw ${expectedYaw}, got ${data.rotation.x}`)
1320
assert.ok(Math.abs(data.rotation.y - expectedPitch) < 0.001,
1321
`Expected pitch ${expectedPitch}, got ${data.rotation.y}`)
1322
done()
1323
}
1324
})
1325
1326
await sleep(100)
1327
bot.entity.yaw = testYaw
1328
bot.entity.pitch = testPitch
1329
bot.activateItem()
1330
})
1331
})
1332
})
1333
})
1334
}
1335
1336