-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMinecraftServer.ts
297 lines (257 loc) · 11 KB
/
MinecraftServer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import { createReader, IReader, Endian } from "bufferstuff";
import { getRandomValues } from "crypto";
import { Console } from "hsconsole";
import { Server, Socket } from "net";
import Config from "../config";
import Chunk from "./Chunk";
import FunkyArray from "funky-array";
import HillyGenerator from "./generators/terrain/Hilly";
import MPClient from "./MPClient";
import NetherGenerator from "./generators/terrain/Nether";
import Packet from "./enums/Packet";
import PacketKeepAlive from "./packets/KeepAlive";
import PacketHandshake from "./packets/Handshake";
import PacketLoginRequest from "./packets/LoginRequest";
import PacketChat from "./packets/Chat";
import PacketSpawnPosition from "./packets/SpawnPosition";
import PacketPlayerPositionLook from "./packets/PlayerPositionLook";
import PacketNamedEntitySpawn from "./packets/NamedEntitySpawn";
import PacketDisconnectKick from "./packets/DisconnectKick";
import PacketTimeUpdate from "./packets/TimeUpdate";
import PacketWindowItems from "./packets/WindowItems";
import Player from "./entities/Player";
import SaveCompressionType from "./enums/SaveCompressionType";
import World from "./World";
import WorldSaveManager from "./WorldSaveManager";
import TextColorParser from "./TextColorParser";
const chunkFrom = -15;
const chunkTo = 15;
const chunkCount = Math.abs(chunkFrom * 2) * (chunkTo * 2);
function getRandomSeed() {
const arr = new Uint32Array(1);
return getRandomValues(arr)[0];
}
export default class MinecraftServer {
private static readonly PROTOCOL_VERSION = 14;
private static readonly TICK_RATE = 20;
private static readonly TICK_RATE_MS = 1000 / MinecraftServer.TICK_RATE;
private readonly keepalivePacket = new PacketKeepAlive().writeData();
private config:Config;
private server:Server;
private readonly serverClock:NodeJS.Timer;
private tickCounter:number = 0;
private clients:FunkyArray<string, MPClient>;
public worlds:FunkyArray<number, World>;
public saveManager:WorldSaveManager;
// https://stackoverflow.com/a/7616484
// Good enough for the world seed.
private hashCode(string:string) : number {
let hash = 0, i, chr;
if (string.length === 0) {
return hash;
}
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return hash;
}
public constructor(config:Config) {
this.config = config;
let shuttingDown = false;
process.on("SIGINT", async (signal) => {
if (shuttingDown) {
return;
}
shuttingDown = true;
Console.printInfo("Shutting down...");
// Stop the server timer
clearInterval(this.serverClock);
// Disconnect all players
const kickPacket = new PacketDisconnectKick("Server shutting down.").writeData();
this.sendToAllClients(kickPacket);
// Save chunks
Console.printInfo("Saving worlds...");
let savedWorldCount = 0;
let savedChunkCount = 0;
const worldsSaveStartTime = Date.now();
await this.worlds.forEach(async (world) => {
const worldSaveStartTime = Date.now();
if (world.chunks.length !== 0) {
await world.chunks.forEach(async (chunk) => {
await world.unloadChunk(Chunk.CreateCoordPair(chunk.x, chunk.z));
savedChunkCount++;
});
}
Console.printInfo(`Saved DIM${world.dimension} to disk. Took ${Date.now() - worldSaveStartTime}ms`);
savedWorldCount++;
});
Console.printInfo(`Saved ${savedChunkCount} chunks from ${savedWorldCount} world(s). Took ${Date.now() - worldsSaveStartTime}ms`);
// Flush final console log to disk and close all writers
Console.cleanup();
// hsconsole is gone now so we have to use built in.
console.log("Goodbye");
// Shut down the tcp server
this.server.close();
});
if (this.config.saveCompression === SaveCompressionType.NONE) {
Console.printWarn("=============- WARNING -=============");
Console.printWarn(" Chunk compression is disabled. This");
Console.printWarn(" will lead to large file sizes!");
Console.printWarn("=====================================");
}
this.clients = new FunkyArray<string, MPClient>();
// Convert seed if needed
let worldSeed = typeof(this.config.seed) === "string" ? this.hashCode(this.config.seed) : typeof(this.config.seed) === "number" ? this.config.seed : getRandomSeed();
// Init save manager and load seed from it if possible
this.saveManager = new WorldSaveManager(this.config, [0, -1], worldSeed);
if (this.saveManager.worldSeed !== Number.MIN_VALUE) {
worldSeed = this.saveManager.worldSeed;
}
this.worlds = new FunkyArray<number, World>();
//this.worlds.set(0, new World(this.saveManager, 0, worldSeed, new NewOverworld(worldSeed)));
this.worlds.set(0, new World(this.saveManager, 0, worldSeed, new HillyGenerator(worldSeed)));
//this.worlds.set(-1, new World(this.saveManager, -1, worldSeed, new NetherGenerator(worldSeed)));
(async () => {
const generateStartTime = Date.now();
let timer = Date.now();
await this.worlds.forEach(async world => {
timer = Date.now();
let chunksGenerated = 0;
let chunksPerSecond = 0;
Console.printInfo(`Generating spawn area for DIM${world.dimension}...`);
for (let x = chunkFrom; x < chunkTo; x++) {
for (let z = chunkFrom; z < chunkTo; z++) {
const chunk = await world.getChunkSafe(x, z);
chunk.forceLoaded = true;
chunksGenerated++;
chunksPerSecond++;
if (Date.now() - timer >= 1000) {
Console.printInfo(`DIM${world.dimension} Progress [${chunksGenerated}/${chunkCount}] (${chunksPerSecond} chunks/s) ${((chunksGenerated / chunkCount) * 100).toFixed(2)}%`);
timer = Date.now();
chunksPerSecond = 0;
}
}
}
});
Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
this.initServer();
}).bind(this)();
const timeUpdate = new PacketTimeUpdate(BigInt(this.tickCounter));
this.serverClock = setInterval(() => {
// Every 1 sec
if (this.tickCounter % MinecraftServer.TICK_RATE === 0) {
if (this.clients.length !== 0) {
timeUpdate.time = BigInt(this.tickCounter);
const timePacket = timeUpdate.writeData();
this.clients.forEach(client => {
// Keep the client happy
client.send(this.keepalivePacket);
client.send(timePacket);
});
}
// const memoryUsage = process.memoryUsage();
// console.log(`Memory Usage: ${(memoryUsage.heapUsed / 1024 / 1024).toFixed(1)}MB / ${(memoryUsage.heapTotal / 1024 / 1024).toFixed(1)}MB ArrayBuffers: ${(memoryUsage.arrayBuffers / 1024 / 1024).toFixed(1)}MB`);
}
this.worlds.forEach(world => {
world.tick();
});
this.tickCounter++;
}, MinecraftServer.TICK_RATE_MS);
this.server = new Server();
this.server.on("connection", this.onConnection.bind(this));
}
initServer() {
this.server.listen(this.config.port, () => Console.printInfo(`Minecraft server started at ${this.config.port}`));
}
sendToAllClients(buffer:Buffer) {
this.clients.forEach(client => {
client.send(buffer);
});
}
sendChatMessage(text:string) {
this.sendToAllClients(new PacketChat(text).writeData());
Console.printInfo(`[CHAT] ${TextColorParser.ParseConsole(text)}`);
}
async handleLoginRequest(reader:IReader, socket:Socket, setMPClient:(mpclient:MPClient) => void) {
const loginPacket = new PacketLoginRequest().readData(reader);
if (loginPacket.protocolVersion !== MinecraftServer.PROTOCOL_VERSION) {
if (loginPacket.protocolVersion > MinecraftServer.PROTOCOL_VERSION) {
socket.write(new PacketDisconnectKick("Outdated server!").writeData());
} else {
socket.write(new PacketDisconnectKick("Outdated or modded client!").writeData());
}
return;
}
const dimension = 0;
const world = this.worlds.get(dimension);
if (world instanceof World) {
const clientEntity = new Player(this, world, loginPacket.username);
if (this.saveManager.playerDataOnDisk.includes(clientEntity.username)) {
clientEntity.fromSave(await this.saveManager.readPlayerDataFromDisk(clientEntity.username));
} else {
clientEntity.position.set(8, 70, 8);
}
world.addEntity(clientEntity);
const client = new MPClient(this, socket, clientEntity);
setMPClient(client);
clientEntity.mpClient = client;
this.clients.set(loginPacket.username, client);
this.sendChatMessage(`\u00a7e${loginPacket.username} joined the game`);
socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, dimension).writeData());
socket.write(new PacketSpawnPosition(8, 64, 8).writeData());
const thisPlayerSpawn = new PacketNamedEntitySpawn(clientEntity.entityId, clientEntity.username, clientEntity.absPosition.x, clientEntity.absPosition.y, clientEntity.absPosition.z, clientEntity.absRotation.yaw, clientEntity.absRotation.pitch, clientEntity.mpClient?.getHeldItemStack()?.itemID).writeData();
world.players.forEach(player => {
if (player.entityId !== clientEntity.entityId && clientEntity.distanceTo(player) < World.ENTITY_MAX_SEND_DISTANCE) {
// Inform the joining player of the players around them
socket.write(new PacketNamedEntitySpawn(player.entityId, player.username, player.absPosition.x, player.absPosition.y, player.absPosition.z, player.absRotation.yaw, player.absRotation.pitch, player.mpClient?.getHeldItemStack()?.itemID).writeData());
player.sendPlayerEquipment(clientEntity);
// Inform players around the joining player of the joined player
player.mpClient?.send(thisPlayerSpawn);
clientEntity.sendPlayerEquipment(player);
}
});
socket.write(new PacketPlayerPositionLook(clientEntity.position.x, clientEntity.position.y, clientEntity.position.y + 0.62, clientEntity.position.z, 0, 0, false).writeData());
const playerInventory = clientEntity.inventory;
socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload(0)).writeData());
} else {
socket.write(new PacketDisconnectKick("Failed to find world to put player in.").writeData());
}
}
handleHandshake(reader:IReader, socket:Socket) {
const handshakePacket = new PacketHandshake().readData(reader);
socket.write(handshakePacket.writeData());
}
onConnection(socket:Socket) {
let mpClient:MPClient;
const setMPClient = (mpclient:MPClient) => {
mpClient = mpclient;
}
const playerDisconnect = (err:Error) => {
mpClient.entity.world.removeEntity(mpClient.entity);
this.clients.remove(mpClient.entity.username);
this.sendChatMessage(`\u00a7e${mpClient.entity.username} left the game`);
if (err) {
Console.printError(`Client disconnected with error: ${err.message}`);
}
}
socket.on("close", playerDisconnect.bind(this));
socket.on("error", playerDisconnect.bind(this));
socket.on("data", chunk => {
const reader = createReader(Endian.BE, chunk);
// Let mpClient take over if it exists
if (mpClient instanceof MPClient) {
mpClient.handlePacket(reader);
return;
}
const packetId = reader.readUByte();
switch (packetId) {
// TODO: Handle timeouts at some point, idk.
case Packet.KeepAlive: break;
case Packet.LoginRequest: this.handleLoginRequest(reader, socket, setMPClient.bind(this)); break;
case Packet.Handshake: this.handleHandshake(reader, socket); break;
}
});
}
}