protocol-classicube 0.6.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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServerConnection
|
|
3
|
+
*
|
|
4
|
+
* Wraps a raw TCP socket from the server's perspective.
|
|
5
|
+
* Handles framing (ClassiCube uses fixed-size packets, no length prefix),
|
|
6
|
+
* dispatches incoming client packets, and exposes send helpers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { EventEmitter } = require('events');
|
|
10
|
+
const PacketIDs = require('../PacketIDs');
|
|
11
|
+
const { BinaryReader } = require('../Binary');
|
|
12
|
+
|
|
13
|
+
// Packets - server -> client
|
|
14
|
+
const ServerIdentification = require('../packets/ServerIdentification');
|
|
15
|
+
const Ping = require('../packets/Ping');
|
|
16
|
+
const LevelInitialize = require('../packets/LevelInitialize');
|
|
17
|
+
const LevelDataChunk = require('../packets/LevelDataChunk');
|
|
18
|
+
const LevelFinalize = require('../packets/LevelFinalize');
|
|
19
|
+
const SpawnPlayer = require('../packets/SpawnPlayer');
|
|
20
|
+
const PlayerTeleport = require('../packets/PlayerTeleport');
|
|
21
|
+
const PlayerUpdate = require('../packets/PlayerUpdate');
|
|
22
|
+
const PlayerMove = require('../packets/PlayerMove');
|
|
23
|
+
const PlayerRotate = require('../packets/PlayerRotate');
|
|
24
|
+
const DespawnPlayer = require('../packets/DespawnPlayer');
|
|
25
|
+
const SetBlock = require('../packets/SetBlock');
|
|
26
|
+
const ChatMessage = require('../packets/ChatMessage');
|
|
27
|
+
const Disconnect = require('../packets/Disconnect');
|
|
28
|
+
const UpdateUserType = require('../packets/UpdateUserType');
|
|
29
|
+
|
|
30
|
+
// Fixed packet sizes for client->server packets
|
|
31
|
+
const CLIENT_PACKET_SIZES = {
|
|
32
|
+
[PacketIDs.CLIENT.PLAYER_IDENTIFICATION]: 131,
|
|
33
|
+
[PacketIDs.CLIENT.SET_BLOCK]: 9,
|
|
34
|
+
[PacketIDs.CLIENT.PLAYER_TELEPORT]: 10,
|
|
35
|
+
[PacketIDs.CLIENT.CHAT_MESSAGE]: 66,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class ServerConnection extends EventEmitter {
|
|
39
|
+
/**
|
|
40
|
+
* @param {import('net').Socket} socket
|
|
41
|
+
*/
|
|
42
|
+
constructor(socket) {
|
|
43
|
+
super();
|
|
44
|
+
this.socket = socket;
|
|
45
|
+
this._buffer = Buffer.alloc(0);
|
|
46
|
+
|
|
47
|
+
// Estado del jugador
|
|
48
|
+
this.id = null;
|
|
49
|
+
this.name = null;
|
|
50
|
+
this.x = 0;
|
|
51
|
+
this.y = 0;
|
|
52
|
+
this.z = 0;
|
|
53
|
+
this.yaw = 0;
|
|
54
|
+
this.pitch = 0;
|
|
55
|
+
this.op = false;
|
|
56
|
+
|
|
57
|
+
socket.on('data', (chunk) => this._onData(chunk));
|
|
58
|
+
socket.on('close', () => this.emit('close'));
|
|
59
|
+
socket.on('error', (err) => this.emit('error', err));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
// Framing
|
|
64
|
+
// ──────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
_onData(chunk) {
|
|
67
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
68
|
+
|
|
69
|
+
while (this._buffer.length > 0) {
|
|
70
|
+
const packetId = this._buffer[0];
|
|
71
|
+
const size = CLIENT_PACKET_SIZES[packetId];
|
|
72
|
+
|
|
73
|
+
if (size === undefined) {
|
|
74
|
+
this.emit('error', new Error(`Unknown client packet ID: 0x${packetId.toString(16)}`));
|
|
75
|
+
this._buffer = Buffer.alloc(0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this._buffer.length < size) break;
|
|
80
|
+
|
|
81
|
+
const raw = this._buffer.slice(0, size);
|
|
82
|
+
this._buffer = this._buffer.slice(size);
|
|
83
|
+
this._dispatch(packetId, raw);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_dispatch(packetId, buf) {
|
|
88
|
+
try {
|
|
89
|
+
switch (packetId) {
|
|
90
|
+
case PacketIDs.CLIENT.PLAYER_IDENTIFICATION: {
|
|
91
|
+
const r = new BinaryReader(buf);
|
|
92
|
+
r.readByte(); // packet id
|
|
93
|
+
const protocolVersion = r.readByte();
|
|
94
|
+
const username = r.readString();
|
|
95
|
+
const verificationKey = r.readString();
|
|
96
|
+
r.readByte(); // unused
|
|
97
|
+
this.name = username;
|
|
98
|
+
this.emit('playerIdentification', { protocolVersion, username, verificationKey });
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case PacketIDs.CLIENT.SET_BLOCK: {
|
|
103
|
+
const pkt = SetBlock.client.deserialize(buf);
|
|
104
|
+
this.emit('setBlock', pkt);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case PacketIDs.CLIENT.PLAYER_TELEPORT: {
|
|
109
|
+
const pkt = PlayerTeleport.deserialize(buf);
|
|
110
|
+
this.x = pkt.x;
|
|
111
|
+
this.y = pkt.y;
|
|
112
|
+
this.z = pkt.z;
|
|
113
|
+
this.yaw = pkt.yaw;
|
|
114
|
+
this.pitch = pkt.pitch;
|
|
115
|
+
this.emit('playerTeleport', pkt);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
case PacketIDs.CLIENT.CHAT_MESSAGE: {
|
|
120
|
+
const pkt = ChatMessage.deserialize(buf);
|
|
121
|
+
this.emit('chatMessage', pkt);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
this.emit('unknownPacket', { packetId, buf });
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.emit('error', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ──────────────────────────────────────────────
|
|
134
|
+
// Write helper
|
|
135
|
+
// ──────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
_write(buf) {
|
|
138
|
+
if (!this.socket.destroyed) this.socket.write(buf);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ──────────────────────────────────────────────
|
|
142
|
+
// Send helpers - Server -> Client
|
|
143
|
+
// ──────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
sendServerIdentification({ name, motd, op = false }) {
|
|
146
|
+
this._write(ServerIdentification.serialize({ name, motd, op }));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
sendPing() {
|
|
150
|
+
this._write(Ping.serialize());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
sendLevelInitialize() {
|
|
154
|
+
this._write(LevelInitialize.serialize());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Envia el mapa completo: LevelInitialize + N LevelDataChunks.
|
|
159
|
+
* Llama a sendLevelFinalize() por separado con las dimensiones.
|
|
160
|
+
* @param {Buffer} gzippedData
|
|
161
|
+
*/
|
|
162
|
+
sendLevel(gzippedData) {
|
|
163
|
+
this.sendLevelInitialize();
|
|
164
|
+
const chunkSize = 1024;
|
|
165
|
+
const total = gzippedData.length;
|
|
166
|
+
for (let offset = 0; offset < total; offset += chunkSize) {
|
|
167
|
+
const slice = gzippedData.slice(offset, offset + chunkSize);
|
|
168
|
+
const percent = Math.min(100, Math.round(((offset + chunkSize) / total) * 100));
|
|
169
|
+
this._write(LevelDataChunk.serialize({
|
|
170
|
+
chunkData: slice,
|
|
171
|
+
chunkLength: slice.length,
|
|
172
|
+
percent,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
sendLevelFinalize({ xSize, ySize, zSize }) {
|
|
178
|
+
this._write(LevelFinalize.serialize({ xSize, ySize, zSize }));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sendSpawnPlayer({ playerId, name, x, y, z, yaw = 0, pitch = 0 }) {
|
|
182
|
+
this._write(SpawnPlayer.serialize({ playerId, name, x, y, z, yaw, pitch }));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
sendTeleport({ playerId, x, y, z, yaw = 0, pitch = 0 }) {
|
|
186
|
+
this._write(PlayerTeleport.serialize({ playerId, x, y, z, yaw, pitch }));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Movimiento relativo + orientacion.
|
|
191
|
+
* Para deltas mayores a ~4 bloques usa sendTeleport().
|
|
192
|
+
*/
|
|
193
|
+
sendPlayerUpdate({ playerId, dx, dy, dz, yaw, pitch }) {
|
|
194
|
+
this._write(PlayerUpdate.serialize({ playerId, dx, dy, dz, yaw, pitch }));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Solo delta de posicion, sin cambio de orientacion. */
|
|
198
|
+
sendPlayerMove({ playerId, dx, dy, dz }) {
|
|
199
|
+
this._write(PlayerMove.serialize({ playerId, dx, dy, dz }));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Solo cambio de orientacion, sin mover posicion. */
|
|
203
|
+
sendPlayerRotate({ playerId, yaw, pitch }) {
|
|
204
|
+
this._write(PlayerRotate.serialize({ playerId, yaw, pitch }));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
sendDespawnPlayer({ playerId }) {
|
|
208
|
+
this._write(DespawnPlayer.serialize({ playerId }));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
sendSetBlock({ x, y, z, blockType }) {
|
|
212
|
+
this._write(SetBlock.server.serialize({ x, y, z, blockType }));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
sendChat(message, playerId = -1) {
|
|
216
|
+
this._write(ChatMessage.serialize({ playerId, message }));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Actualiza los permisos del jugador en tiempo real. */
|
|
220
|
+
sendUpdateUserType(op) {
|
|
221
|
+
this.op = op;
|
|
222
|
+
this._write(UpdateUserType.serialize({ op }));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
sendDisconnect(reason = 'Disconnected') {
|
|
226
|
+
this._write(Disconnect.serialize({ reason }));
|
|
227
|
+
this.socket.end();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
kick(reason = 'Kicked') {
|
|
231
|
+
this.sendDisconnect(reason);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Inicia un loop de ping automatico.
|
|
236
|
+
* @param {number} [intervalMs=5000]
|
|
237
|
+
* @returns {NodeJS.Timeout}
|
|
238
|
+
*/
|
|
239
|
+
startPingLoop(intervalMs = 5000) {
|
|
240
|
+
const interval = setInterval(() => {
|
|
241
|
+
if (this.socket.destroyed) return clearInterval(interval);
|
|
242
|
+
this.sendPing();
|
|
243
|
+
}, intervalMs);
|
|
244
|
+
this.once('close', () => clearInterval(interval));
|
|
245
|
+
return interval;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = ServerConnection;
|