protocol-classicube 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Binary.js +149 -0
- package/PacketIDs.js +35 -0
- package/client/ClientConnection.js +308 -0
- package/index.js +62 -0
- package/package.json +29 -0
- package/packets/ChatMessage.js +48 -0
- package/packets/DespawnPlayer.js +38 -0
- package/packets/Disconnect.js +36 -0
- package/packets/LevelDataChunk.js +47 -0
- package/packets/LevelFinalize.js +44 -0
- package/packets/LevelInitialize.js +29 -0
- package/packets/Ping.js +29 -0
- package/packets/PlayerIdentification.js +48 -0
- package/packets/PlayerMove.js +52 -0
- package/packets/PlayerRotate.js +46 -0
- package/packets/PlayerTeleport.js +59 -0
- package/packets/PlayerUpdate.js +62 -0
- package/packets/ServerIdentification.js +63 -0
- package/packets/SetBlock.js +93 -0
- package/packets/SpawnPlayer.js +62 -0
- package/packets/UpdateUserType.js +51 -0
- package/server/ServerConnection.js +249 -0
package/Binary.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassiCube Protocol - Binary Read/Write Helpers
|
|
3
|
+
*
|
|
4
|
+
* Classic protocol types:
|
|
5
|
+
* Byte - 1 byte unsigned
|
|
6
|
+
* SByte - 1 byte signed
|
|
7
|
+
* Short - 2 bytes signed big-endian
|
|
8
|
+
* Int - 4 bytes signed big-endian
|
|
9
|
+
* String - 64 bytes, padded with spaces (0x20), ASCII
|
|
10
|
+
* ByteArray - 1024 bytes of raw data
|
|
11
|
+
* FByte - Fixed-point: value / 32 for positions
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class BinaryWriter {
|
|
15
|
+
constructor() {
|
|
16
|
+
this._chunks = [];
|
|
17
|
+
this.length = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
writeByte(value) {
|
|
21
|
+
const buf = Buffer.alloc(1);
|
|
22
|
+
buf.writeUInt8(value & 0xFF, 0);
|
|
23
|
+
this._chunks.push(buf);
|
|
24
|
+
this.length += 1;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
writeSByte(value) {
|
|
29
|
+
const buf = Buffer.alloc(1);
|
|
30
|
+
buf.writeInt8(value, 0);
|
|
31
|
+
this._chunks.push(buf);
|
|
32
|
+
this.length += 1;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
writeShort(value) {
|
|
37
|
+
const buf = Buffer.alloc(2);
|
|
38
|
+
buf.writeInt16BE(value, 0);
|
|
39
|
+
this._chunks.push(buf);
|
|
40
|
+
this.length += 2;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeInt(value) {
|
|
45
|
+
const buf = Buffer.alloc(4);
|
|
46
|
+
buf.writeInt32BE(value, 0);
|
|
47
|
+
this._chunks.push(buf);
|
|
48
|
+
this.length += 4;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Classic string: always 64 bytes, space-padded
|
|
54
|
+
*/
|
|
55
|
+
writeString(value) {
|
|
56
|
+
const buf = Buffer.alloc(64, 0x20); // fill with spaces
|
|
57
|
+
const encoded = Buffer.from(String(value).substring(0, 64), 'ascii');
|
|
58
|
+
encoded.copy(buf, 0);
|
|
59
|
+
this._chunks.push(buf);
|
|
60
|
+
this.length += 64;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ByteArray: always 1024 bytes
|
|
66
|
+
*/
|
|
67
|
+
writeByteArray(data) {
|
|
68
|
+
const buf = Buffer.alloc(1024, 0x00);
|
|
69
|
+
if (Buffer.isBuffer(data)) {
|
|
70
|
+
data.copy(buf, 0, 0, Math.min(data.length, 1024));
|
|
71
|
+
}
|
|
72
|
+
this._chunks.push(buf);
|
|
73
|
+
this.length += 1024;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fixed-point position byte: value * 32
|
|
79
|
+
*/
|
|
80
|
+
writeFByte(value) {
|
|
81
|
+
return this.writeSByte(Math.round(value * 32));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
toBuffer() {
|
|
85
|
+
return Buffer.concat(this._chunks);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class BinaryReader {
|
|
90
|
+
constructor(buffer) {
|
|
91
|
+
this._buf = buffer;
|
|
92
|
+
this._offset = 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get bytesLeft() {
|
|
96
|
+
return this._buf.length - this._offset;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
readByte() {
|
|
100
|
+
const val = this._buf.readUInt8(this._offset);
|
|
101
|
+
this._offset += 1;
|
|
102
|
+
return val;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
readSByte() {
|
|
106
|
+
const val = this._buf.readInt8(this._offset);
|
|
107
|
+
this._offset += 1;
|
|
108
|
+
return val;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
readShort() {
|
|
112
|
+
const val = this._buf.readInt16BE(this._offset);
|
|
113
|
+
this._offset += 2;
|
|
114
|
+
return val;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
readInt() {
|
|
118
|
+
const val = this._buf.readInt32BE(this._offset);
|
|
119
|
+
this._offset += 4;
|
|
120
|
+
return val;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Classic string: read 64 bytes, trim trailing spaces
|
|
125
|
+
*/
|
|
126
|
+
readString() {
|
|
127
|
+
const buf = this._buf.slice(this._offset, this._offset + 64);
|
|
128
|
+
this._offset += 64;
|
|
129
|
+
return buf.toString('ascii').trimEnd();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* ByteArray: read 1024 bytes
|
|
134
|
+
*/
|
|
135
|
+
readByteArray() {
|
|
136
|
+
const buf = this._buf.slice(this._offset, this._offset + 1024);
|
|
137
|
+
this._offset += 1024;
|
|
138
|
+
return Buffer.from(buf);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fixed-point position byte: raw / 32
|
|
143
|
+
*/
|
|
144
|
+
readFByte() {
|
|
145
|
+
return this.readSByte() / 32;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { BinaryWriter, BinaryReader };
|
package/PacketIDs.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClassiCube Protocol - Packet IDs
|
|
3
|
+
* Based on the Classic Minecraft / ClassiCube protocol spec
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PacketIDs = {
|
|
7
|
+
// Server -> Client
|
|
8
|
+
SERVER: {
|
|
9
|
+
SERVER_IDENTIFICATION: 0x00,
|
|
10
|
+
PING: 0x01,
|
|
11
|
+
LEVEL_INITIALIZE: 0x02,
|
|
12
|
+
LEVEL_DATA_CHUNK: 0x03,
|
|
13
|
+
LEVEL_FINALIZE: 0x04,
|
|
14
|
+
SET_BLOCK: 0x06,
|
|
15
|
+
SPAWN_PLAYER: 0x07,
|
|
16
|
+
PLAYER_TELEPORT: 0x08,
|
|
17
|
+
PLAYER_UPDATE: 0x09,
|
|
18
|
+
PLAYER_MOVE: 0x0A,
|
|
19
|
+
PLAYER_ROTATE: 0x0B,
|
|
20
|
+
DESPAWN_PLAYER: 0x0C,
|
|
21
|
+
CHAT_MESSAGE: 0x0D,
|
|
22
|
+
DISCONNECT: 0x0E,
|
|
23
|
+
UPDATE_USER_TYPE: 0x0F,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Client -> Server
|
|
27
|
+
CLIENT: {
|
|
28
|
+
PLAYER_IDENTIFICATION: 0x00,
|
|
29
|
+
SET_BLOCK: 0x05,
|
|
30
|
+
PLAYER_TELEPORT: 0x08,
|
|
31
|
+
CHAT_MESSAGE: 0x0D,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = PacketIDs;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientConnection
|
|
3
|
+
*
|
|
4
|
+
* Wraps a raw TCP socket from the client's perspective.
|
|
5
|
+
* Handles framing, dispatches ALL server packets,
|
|
6
|
+
* and exposes send helpers for client->server packets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { EventEmitter } = require('events');
|
|
10
|
+
const net = require('net');
|
|
11
|
+
const PacketIDs = require('../PacketIDs');
|
|
12
|
+
const { BinaryWriter } = require('../Binary');
|
|
13
|
+
|
|
14
|
+
// Packet deserializers
|
|
15
|
+
const ServerIdentification = require('../packets/ServerIdentification');
|
|
16
|
+
const Ping = require('../packets/Ping');
|
|
17
|
+
const LevelInitialize = require('../packets/LevelInitialize');
|
|
18
|
+
const LevelDataChunk = require('../packets/LevelDataChunk');
|
|
19
|
+
const LevelFinalize = require('../packets/LevelFinalize');
|
|
20
|
+
const SpawnPlayer = require('../packets/SpawnPlayer');
|
|
21
|
+
const PlayerTeleport = require('../packets/PlayerTeleport');
|
|
22
|
+
const PlayerUpdate = require('../packets/PlayerUpdate');
|
|
23
|
+
const PlayerMove = require('../packets/PlayerMove');
|
|
24
|
+
const PlayerRotate = require('../packets/PlayerRotate');
|
|
25
|
+
const DespawnPlayer = require('../packets/DespawnPlayer');
|
|
26
|
+
const SetBlock = require('../packets/SetBlock');
|
|
27
|
+
const ChatMessage = require('../packets/ChatMessage');
|
|
28
|
+
const Disconnect = require('../packets/Disconnect');
|
|
29
|
+
const UpdateUserType = require('../packets/UpdateUserType');
|
|
30
|
+
|
|
31
|
+
// Fixed packet sizes for server->client packets
|
|
32
|
+
const SERVER_PACKET_SIZES = {
|
|
33
|
+
[PacketIDs.SERVER.SERVER_IDENTIFICATION]: 131,
|
|
34
|
+
[PacketIDs.SERVER.PING]: 1,
|
|
35
|
+
[PacketIDs.SERVER.LEVEL_INITIALIZE]: 1,
|
|
36
|
+
[PacketIDs.SERVER.LEVEL_DATA_CHUNK]: 1028,
|
|
37
|
+
[PacketIDs.SERVER.LEVEL_FINALIZE]: 7,
|
|
38
|
+
[PacketIDs.SERVER.SET_BLOCK]: 8,
|
|
39
|
+
[PacketIDs.SERVER.SPAWN_PLAYER]: 74,
|
|
40
|
+
[PacketIDs.SERVER.PLAYER_TELEPORT]: 10,
|
|
41
|
+
[PacketIDs.SERVER.PLAYER_UPDATE]: 7,
|
|
42
|
+
[PacketIDs.SERVER.PLAYER_MOVE]: 5,
|
|
43
|
+
[PacketIDs.SERVER.PLAYER_ROTATE]: 4,
|
|
44
|
+
[PacketIDs.SERVER.DESPAWN_PLAYER]: 2,
|
|
45
|
+
[PacketIDs.SERVER.CHAT_MESSAGE]: 66,
|
|
46
|
+
[PacketIDs.SERVER.DISCONNECT]: 65,
|
|
47
|
+
[PacketIDs.SERVER.UPDATE_USER_TYPE]: 2,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class ClientConnection extends EventEmitter {
|
|
51
|
+
constructor() {
|
|
52
|
+
super();
|
|
53
|
+
this.socket = null;
|
|
54
|
+
this._buffer = Buffer.alloc(0);
|
|
55
|
+
|
|
56
|
+
// Re-ensamble del level
|
|
57
|
+
this._levelChunks = [];
|
|
58
|
+
this._receivingLevel = false;
|
|
59
|
+
|
|
60
|
+
// Estado local del cliente
|
|
61
|
+
this.x = 0;
|
|
62
|
+
this.y = 0;
|
|
63
|
+
this.z = 0;
|
|
64
|
+
this.yaw = 0;
|
|
65
|
+
this.pitch = 0;
|
|
66
|
+
this.op = false;
|
|
67
|
+
|
|
68
|
+
// Entidades de otros jugadores: Map<playerId, { name, x, y, z, yaw, pitch }>
|
|
69
|
+
this.players = new Map();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Conectar a un servidor ClassiCube.
|
|
74
|
+
* @param {string} host
|
|
75
|
+
* @param {number} port
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
connect(host, port) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
this.socket = net.createConnection({ host, port }, () => resolve());
|
|
81
|
+
this.socket.on('data', (chunk) => this._onData(chunk));
|
|
82
|
+
this.socket.on('close', () => this.emit('close'));
|
|
83
|
+
this.socket.on('error', (err) => { this.emit('error', err); reject(err); });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
disconnect() {
|
|
88
|
+
if (this.socket && !this.socket.destroyed) this.socket.destroy();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ──────────────────────────────────────────────
|
|
92
|
+
// Framing
|
|
93
|
+
// ──────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
_onData(chunk) {
|
|
96
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
97
|
+
|
|
98
|
+
while (this._buffer.length > 0) {
|
|
99
|
+
const packetId = this._buffer[0];
|
|
100
|
+
const size = SERVER_PACKET_SIZES[packetId];
|
|
101
|
+
|
|
102
|
+
if (size === undefined) {
|
|
103
|
+
this.emit('error', new Error(`Unknown server packet ID: 0x${packetId.toString(16)}`));
|
|
104
|
+
this._buffer = Buffer.alloc(0);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this._buffer.length < size) break;
|
|
109
|
+
|
|
110
|
+
const raw = this._buffer.slice(0, size);
|
|
111
|
+
this._buffer = this._buffer.slice(size);
|
|
112
|
+
this._dispatch(packetId, raw);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_dispatch(packetId, buf) {
|
|
117
|
+
try {
|
|
118
|
+
switch (packetId) {
|
|
119
|
+
|
|
120
|
+
case PacketIDs.SERVER.SERVER_IDENTIFICATION: {
|
|
121
|
+
const pkt = ServerIdentification.deserialize(buf);
|
|
122
|
+
this.op = pkt.op;
|
|
123
|
+
this.emit('serverIdentification', pkt);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case PacketIDs.SERVER.PING:
|
|
128
|
+
this.emit('ping');
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case PacketIDs.SERVER.LEVEL_INITIALIZE:
|
|
132
|
+
this._levelChunks = [];
|
|
133
|
+
this._receivingLevel = true;
|
|
134
|
+
this.emit('levelInitialize');
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case PacketIDs.SERVER.LEVEL_DATA_CHUNK: {
|
|
138
|
+
const pkt = LevelDataChunk.deserialize(buf);
|
|
139
|
+
if (this._receivingLevel) {
|
|
140
|
+
this._levelChunks.push(pkt.chunkData.slice(0, pkt.chunkLength));
|
|
141
|
+
}
|
|
142
|
+
this.emit('levelDataChunk', pkt);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
case PacketIDs.SERVER.LEVEL_FINALIZE: {
|
|
147
|
+
const pkt = LevelFinalize.deserialize(buf);
|
|
148
|
+
const levelData = Buffer.concat(this._levelChunks);
|
|
149
|
+
this._receivingLevel = false;
|
|
150
|
+
this._levelChunks = [];
|
|
151
|
+
this.emit('levelFinalize', { ...pkt, levelData });
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case PacketIDs.SERVER.SET_BLOCK:
|
|
156
|
+
this.emit('setBlock', SetBlock.server.deserialize(buf));
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case PacketIDs.SERVER.SPAWN_PLAYER: {
|
|
160
|
+
const pkt = SpawnPlayer.deserialize(buf);
|
|
161
|
+
if (pkt.playerId === -1) {
|
|
162
|
+
// Somos nosotros
|
|
163
|
+
this.x = pkt.x; this.y = pkt.y; this.z = pkt.z;
|
|
164
|
+
this.yaw = pkt.yaw; this.pitch = pkt.pitch;
|
|
165
|
+
} else {
|
|
166
|
+
this.players.set(pkt.playerId, {
|
|
167
|
+
name: pkt.name,
|
|
168
|
+
x: pkt.x, y: pkt.y, z: pkt.z,
|
|
169
|
+
yaw: pkt.yaw, pitch: pkt.pitch,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
this.emit('spawnPlayer', pkt);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case PacketIDs.SERVER.PLAYER_TELEPORT: {
|
|
177
|
+
const pkt = PlayerTeleport.deserialize(buf);
|
|
178
|
+
if (pkt.playerId === -1) {
|
|
179
|
+
this.x = pkt.x; this.y = pkt.y; this.z = pkt.z;
|
|
180
|
+
this.yaw = pkt.yaw; this.pitch = pkt.pitch;
|
|
181
|
+
} else {
|
|
182
|
+
const p = this.players.get(pkt.playerId);
|
|
183
|
+
if (p) {
|
|
184
|
+
p.x = pkt.x; p.y = pkt.y; p.z = pkt.z;
|
|
185
|
+
p.yaw = pkt.yaw; p.pitch = pkt.pitch;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.emit('playerTeleport', pkt);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case PacketIDs.SERVER.PLAYER_UPDATE: {
|
|
193
|
+
const pkt = PlayerUpdate.deserialize(buf);
|
|
194
|
+
const p = this.players.get(pkt.playerId);
|
|
195
|
+
if (p) {
|
|
196
|
+
p.x += pkt.dx; p.y += pkt.dy; p.z += pkt.dz;
|
|
197
|
+
p.yaw = pkt.yaw; p.pitch = pkt.pitch;
|
|
198
|
+
}
|
|
199
|
+
this.emit('playerUpdate', pkt);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case PacketIDs.SERVER.PLAYER_MOVE: {
|
|
204
|
+
const pkt = PlayerMove.deserialize(buf);
|
|
205
|
+
const p = this.players.get(pkt.playerId);
|
|
206
|
+
if (p) { p.x += pkt.dx; p.y += pkt.dy; p.z += pkt.dz; }
|
|
207
|
+
this.emit('playerMove', pkt);
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case PacketIDs.SERVER.PLAYER_ROTATE: {
|
|
212
|
+
const pkt = PlayerRotate.deserialize(buf);
|
|
213
|
+
const p = this.players.get(pkt.playerId);
|
|
214
|
+
if (p) { p.yaw = pkt.yaw; p.pitch = pkt.pitch; }
|
|
215
|
+
this.emit('playerRotate', pkt);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case PacketIDs.SERVER.DESPAWN_PLAYER: {
|
|
220
|
+
const pkt = DespawnPlayer.deserialize(buf);
|
|
221
|
+
this.players.delete(pkt.playerId);
|
|
222
|
+
this.emit('despawnPlayer', pkt);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
case PacketIDs.SERVER.CHAT_MESSAGE:
|
|
227
|
+
this.emit('chatMessage', ChatMessage.deserialize(buf));
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case PacketIDs.SERVER.DISCONNECT: {
|
|
231
|
+
const pkt = Disconnect.deserialize(buf);
|
|
232
|
+
this.emit('disconnect', pkt);
|
|
233
|
+
this.disconnect();
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case PacketIDs.SERVER.UPDATE_USER_TYPE: {
|
|
238
|
+
const pkt = UpdateUserType.deserialize(buf);
|
|
239
|
+
this.op = pkt.op;
|
|
240
|
+
this.emit('updateUserType', pkt);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
default:
|
|
245
|
+
this.emit('unknownPacket', { packetId, buf });
|
|
246
|
+
}
|
|
247
|
+
} catch (err) {
|
|
248
|
+
this.emit('error', err);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ──────────────────────────────────────────────
|
|
253
|
+
// Write helper
|
|
254
|
+
// ──────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
_write(buf) {
|
|
257
|
+
if (this.socket && !this.socket.destroyed) this.socket.write(buf);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ──────────────────────────────────────────────
|
|
261
|
+
// Send helpers - Client -> Server
|
|
262
|
+
// ──────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Login handshake. Primer paquete que debe enviarse.
|
|
266
|
+
* @param {object} opts
|
|
267
|
+
* @param {string} opts.username
|
|
268
|
+
* @param {string} [opts.verificationKey='-']
|
|
269
|
+
*/
|
|
270
|
+
sendIdentification({ username, verificationKey = '-' }) {
|
|
271
|
+
const buf = new BinaryWriter()
|
|
272
|
+
.writeByte(PacketIDs.CLIENT.PLAYER_IDENTIFICATION)
|
|
273
|
+
.writeByte(7) // protocol version
|
|
274
|
+
.writeString(username)
|
|
275
|
+
.writeString(verificationKey)
|
|
276
|
+
.writeByte(0x00) // unused
|
|
277
|
+
.toBuffer();
|
|
278
|
+
this._write(buf);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {object} opts
|
|
283
|
+
* @param {number} opts.x
|
|
284
|
+
* @param {number} opts.y
|
|
285
|
+
* @param {number} opts.z
|
|
286
|
+
* @param {number} opts.mode - 0 = destruir, 1 = colocar
|
|
287
|
+
* @param {number} opts.blockType - ID del bloque
|
|
288
|
+
*/
|
|
289
|
+
sendSetBlock({ x, y, z, mode, blockType }) {
|
|
290
|
+
this._write(SetBlock.client.serialize({ x, y, z, mode, blockType }));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Actualiza la posicion del cliente en el servidor.
|
|
295
|
+
*/
|
|
296
|
+
sendTeleport({ x, y, z, yaw = 0, pitch = 0 }) {
|
|
297
|
+
// playerId = -1 (self) cuando el cliente envia su propia posicion
|
|
298
|
+
this._write(PlayerTeleport.serialize({ playerId: -1, x, y, z, yaw, pitch }));
|
|
299
|
+
this.x = x; this.y = y; this.z = z;
|
|
300
|
+
this.yaw = yaw; this.pitch = pitch;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
sendChat(message) {
|
|
304
|
+
this._write(ChatMessage.serialize({ playerId: -1, message }));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = ClientConnection;
|
package/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* classicube-protocol
|
|
3
|
+
*
|
|
4
|
+
* Implementacion completa del protocolo ClassiCube / Classic Minecraft.
|
|
5
|
+
* Protocol version 7. Sin dependencias externas. Node.js 14+.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const PacketIDs = require('./PacketIDs');
|
|
11
|
+
const { BinaryWriter, BinaryReader } = require('./Binary');
|
|
12
|
+
const ServerConnection = require('./server/ServerConnection');
|
|
13
|
+
const ClientConnection = require('./client/ClientConnection');
|
|
14
|
+
|
|
15
|
+
// Packets
|
|
16
|
+
const PlayerIdentification = require('./packets/PlayerIdentification');
|
|
17
|
+
const ServerIdentification = require('./packets/ServerIdentification');
|
|
18
|
+
const Ping = require('./packets/Ping');
|
|
19
|
+
const LevelInitialize = require('./packets/LevelInitialize');
|
|
20
|
+
const LevelDataChunk = require('./packets/LevelDataChunk');
|
|
21
|
+
const LevelFinalize = require('./packets/LevelFinalize');
|
|
22
|
+
const SpawnPlayer = require('./packets/SpawnPlayer');
|
|
23
|
+
const PlayerTeleport = require('./packets/PlayerTeleport');
|
|
24
|
+
const PlayerUpdate = require('./packets/PlayerUpdate');
|
|
25
|
+
const PlayerMove = require('./packets/PlayerMove');
|
|
26
|
+
const PlayerRotate = require('./packets/PlayerRotate');
|
|
27
|
+
const DespawnPlayer = require('./packets/DespawnPlayer');
|
|
28
|
+
const SetBlock = require('./packets/SetBlock');
|
|
29
|
+
const ChatMessage = require('./packets/ChatMessage');
|
|
30
|
+
const Disconnect = require('./packets/Disconnect');
|
|
31
|
+
const UpdateUserType = require('./packets/UpdateUserType');
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
// Utilidades core
|
|
35
|
+
PacketIDs,
|
|
36
|
+
BinaryWriter,
|
|
37
|
+
BinaryReader,
|
|
38
|
+
|
|
39
|
+
// Conexiones de alto nivel
|
|
40
|
+
ServerConnection,
|
|
41
|
+
ClientConnection,
|
|
42
|
+
|
|
43
|
+
// Todos los paquetes
|
|
44
|
+
packets: {
|
|
45
|
+
PlayerIdentification,
|
|
46
|
+
ServerIdentification,
|
|
47
|
+
Ping,
|
|
48
|
+
LevelInitialize,
|
|
49
|
+
LevelDataChunk,
|
|
50
|
+
LevelFinalize,
|
|
51
|
+
SpawnPlayer,
|
|
52
|
+
PlayerTeleport,
|
|
53
|
+
PlayerUpdate,
|
|
54
|
+
PlayerMove,
|
|
55
|
+
PlayerRotate,
|
|
56
|
+
DespawnPlayer,
|
|
57
|
+
SetBlock,
|
|
58
|
+
ChatMessage,
|
|
59
|
+
Disconnect,
|
|
60
|
+
UpdateUserType,
|
|
61
|
+
},
|
|
62
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "protocol-classicube",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "ClassiCube / Classic Minecraft protocol implementation for Node.js. No dependencies.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"classicube",
|
|
9
|
+
"minecraft",
|
|
10
|
+
"minecraft-classic",
|
|
11
|
+
"protocol",
|
|
12
|
+
"network",
|
|
13
|
+
"server",
|
|
14
|
+
"client"
|
|
15
|
+
],
|
|
16
|
+
"author": "Rosendo Torre",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=14.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"Binary.js",
|
|
24
|
+
"PacketIDs.js",
|
|
25
|
+
"packets/",
|
|
26
|
+
"client/",
|
|
27
|
+
"server/"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packet 0x0D - Chat Message (bidirectional)
|
|
3
|
+
*
|
|
4
|
+
* Server -> Client:
|
|
5
|
+
* Player ID -1 (0xFF) = server message
|
|
6
|
+
* Other IDs = player chat
|
|
7
|
+
*
|
|
8
|
+
* Client -> Server:
|
|
9
|
+
* Player ID is always 0xFF (unused by server but must be sent)
|
|
10
|
+
*
|
|
11
|
+
* Layout:
|
|
12
|
+
* Byte - Packet ID (0x0D)
|
|
13
|
+
* SByte - Player ID
|
|
14
|
+
* String - Message (64 bytes)
|
|
15
|
+
*
|
|
16
|
+
* Total: 66 bytes
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { BinaryWriter, BinaryReader } = require('../Binary');
|
|
20
|
+
const PacketIDs = require('../PacketIDs');
|
|
21
|
+
|
|
22
|
+
const SERVER_MESSAGE_ID = -1;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {number} opts.playerId - -1 for server, player ID otherwise
|
|
27
|
+
* @param {string} opts.message
|
|
28
|
+
*/
|
|
29
|
+
function serialize({ playerId = SERVER_MESSAGE_ID, message }) {
|
|
30
|
+
return new BinaryWriter()
|
|
31
|
+
.writeByte(PacketIDs.SERVER.CHAT_MESSAGE)
|
|
32
|
+
.writeSByte(playerId)
|
|
33
|
+
.writeString(message)
|
|
34
|
+
.toBuffer();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function deserialize(buf) {
|
|
38
|
+
const r = new BinaryReader(buf);
|
|
39
|
+
const packetId = r.readByte();
|
|
40
|
+
if (packetId !== PacketIDs.SERVER.CHAT_MESSAGE) {
|
|
41
|
+
throw new Error(`Expected ChatMessage (0x0D), got 0x${packetId.toString(16)}`);
|
|
42
|
+
}
|
|
43
|
+
const playerId = r.readSByte();
|
|
44
|
+
const message = r.readString();
|
|
45
|
+
return { packetId, playerId, message };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { serialize, deserialize, PACKET_SIZE: 66, SERVER_MESSAGE_ID };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packet 0x0C - Despawn Player (Server -> Client)
|
|
3
|
+
*
|
|
4
|
+
* Elimina la entidad de un jugador del mundo del cliente.
|
|
5
|
+
* Se envia cuando un jugador se desconecta o cambia de mundo.
|
|
6
|
+
*
|
|
7
|
+
* Layout:
|
|
8
|
+
* Byte - Packet ID (0x0C)
|
|
9
|
+
* SByte - Player ID
|
|
10
|
+
*
|
|
11
|
+
* Total: 2 bytes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { BinaryWriter, BinaryReader } = require('../Binary');
|
|
15
|
+
const PacketIDs = require('../PacketIDs');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {number} opts.playerId
|
|
20
|
+
*/
|
|
21
|
+
function serialize({ playerId }) {
|
|
22
|
+
return new BinaryWriter()
|
|
23
|
+
.writeByte(PacketIDs.SERVER.DESPAWN_PLAYER)
|
|
24
|
+
.writeSByte(playerId)
|
|
25
|
+
.toBuffer();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function deserialize(buf) {
|
|
29
|
+
const r = new BinaryReader(buf);
|
|
30
|
+
const packetId = r.readByte();
|
|
31
|
+
if (packetId !== PacketIDs.SERVER.DESPAWN_PLAYER) {
|
|
32
|
+
throw new Error(`Expected DespawnPlayer (0x0C), got 0x${packetId.toString(16)}`);
|
|
33
|
+
}
|
|
34
|
+
const playerId = r.readSByte();
|
|
35
|
+
return { packetId, playerId };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { serialize, deserialize, PACKET_SIZE: 2 };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packet 0x0E - Disconnect (Server -> Client)
|
|
3
|
+
*
|
|
4
|
+
* Kicks the player with a reason.
|
|
5
|
+
* Layout:
|
|
6
|
+
* Byte - Packet ID (0x0E)
|
|
7
|
+
* String - Disconnect reason (64 bytes)
|
|
8
|
+
*
|
|
9
|
+
* Total: 65 bytes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { BinaryWriter, BinaryReader } = require('../Binary');
|
|
13
|
+
const PacketIDs = require('../PacketIDs');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} opts
|
|
17
|
+
* @param {string} opts.reason
|
|
18
|
+
*/
|
|
19
|
+
function serialize({ reason }) {
|
|
20
|
+
return new BinaryWriter()
|
|
21
|
+
.writeByte(PacketIDs.SERVER.DISCONNECT)
|
|
22
|
+
.writeString(reason)
|
|
23
|
+
.toBuffer();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function deserialize(buf) {
|
|
27
|
+
const r = new BinaryReader(buf);
|
|
28
|
+
const packetId = r.readByte();
|
|
29
|
+
if (packetId !== PacketIDs.SERVER.DISCONNECT) {
|
|
30
|
+
throw new Error(`Expected Disconnect (0x0E), got 0x${packetId.toString(16)}`);
|
|
31
|
+
}
|
|
32
|
+
const reason = r.readString();
|
|
33
|
+
return { packetId, reason };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { serialize, deserialize, PACKET_SIZE: 65 };
|