mc-beta-server/server/MinecraftServer.ts

297 lines
11 KiB
TypeScript
Raw Normal View History

2023-06-22 12:43:28 +01:00
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";
2023-04-08 20:52:47 +01:00
2023-11-08 13:36:22 +00:00
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 {
2023-04-08 20:52:47 +01:00
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;
2023-08-20 01:18:05 +01:00
private readonly serverClock:NodeJS.Timeout;
2023-04-08 20:52:47 +01:00
private tickCounter:number = 0;
2023-04-09 04:19:10 +01:00
private clients:FunkyArray<string, MPClient>;
2023-09-04 23:42:38 +01:00
public worlds:FunkyArray<number, World>;
2023-04-11 07:47:56 +01:00
public saveManager:WorldSaveManager;
2023-04-09 04:19:10 +01:00
// 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;
}
2023-04-08 20:52:47 +01:00
public constructor(config:Config) {
this.config = config;
2023-08-20 01:18:05 +01:00
let shuttingDown = false;
process.on("SIGINT", async (signal) => {
2023-08-20 01:18:05 +01:00
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);
2024-10-26 14:38:32 +01:00
// Save chunks
Console.printInfo("Saving worlds...");
let savedWorldCount = 0;
let savedChunkCount = 0;
2024-10-26 14:38:32 +01:00
const worldsSaveStartTime = Date.now();
await this.worlds.forEach(async (world) => {
2024-10-26 14:38:32 +01:00
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++;
});
}
2024-10-26 14:38:32 +01:00
Console.printInfo(`Saved DIM${world.dimension} to disk. Took ${Date.now() - worldSaveStartTime}ms`);
savedWorldCount++;
});
2024-10-26 14:38:32 +01:00
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");
2024-10-26 14:38:32 +01:00
// Shut down the tcp server
this.server.close();
});
if (this.config.saveCompression === SaveCompressionType.NONE) {
2023-04-11 07:47:56 +01:00
Console.printWarn("=============- WARNING -=============");
Console.printWarn(" Chunk compression is disabled. This");
Console.printWarn(" will lead to large file sizes!");
Console.printWarn("=====================================");
}
2023-04-09 04:19:10 +01:00
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();
2023-04-11 07:47:56 +01:00
// Init save manager and load seed from it if possible
this.saveManager = new WorldSaveManager(this.config, [0, -1], worldSeed);
2023-04-11 07:47:56 +01:00
if (this.saveManager.worldSeed !== Number.MIN_VALUE) {
worldSeed = this.saveManager.worldSeed;
}
2023-04-08 20:52:47 +01:00
this.worlds = new FunkyArray<number, World>();
2024-07-09 21:52:13 +01:00
//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)));
2023-06-19 18:29:16 +01:00
(async () => {
const generateStartTime = Date.now();
let timer = Date.now();
2023-11-08 13:36:22 +00:00
await this.worlds.forEach(async world => {
2023-11-08 15:44:46 +00:00
timer = Date.now();
2023-11-08 13:36:22 +00:00
let chunksGenerated = 0;
2023-11-08 15:44:46 +00:00
let chunksPerSecond = 0;
2023-11-08 13:36:22 +00:00
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++;
2023-11-08 15:44:46 +00:00
chunksPerSecond++;
2023-11-08 13:36:22 +00:00
if (Date.now() - timer >= 1000) {
2023-11-08 15:44:46 +00:00
Console.printInfo(`DIM${world.dimension} Progress [${chunksGenerated}/${chunkCount}] (${chunksPerSecond} chunks/s) ${((chunksGenerated / chunkCount) * 100).toFixed(2)}%`);
2023-11-08 13:36:22 +00:00
timer = Date.now();
2023-11-08 15:44:46 +00:00
chunksPerSecond = 0;
2023-11-08 13:36:22 +00:00
}
}
}
2023-11-08 13:36:22 +00:00
});
2023-06-19 18:29:16 +01:00
Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
this.initServer();
2023-04-11 07:47:56 +01:00
}).bind(this)();
2023-04-08 20:52:47 +01:00
2023-11-08 13:36:22 +00:00
const timeUpdate = new PacketTimeUpdate(BigInt(this.tickCounter));
2023-04-08 20:52:47 +01:00
this.serverClock = setInterval(() => {
// Every 1 sec
if (this.tickCounter % MinecraftServer.TICK_RATE === 0) {
if (this.clients.length !== 0) {
2023-11-08 13:36:22 +00:00
timeUpdate.time = BigInt(this.tickCounter);
const timePacket = timeUpdate.writeData();
2023-04-08 20:52:47 +01:00
this.clients.forEach(client => {
2023-08-20 01:18:05 +01:00
// Keep the client happy
2023-04-08 20:52:47 +01:00
client.send(this.keepalivePacket);
2023-08-20 01:18:05 +01:00
client.send(timePacket);
2023-04-08 20:52:47 +01:00
});
}
2023-11-05 10:58:35 +00:00
// 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`);
2023-04-08 20:52:47 +01:00
}
this.worlds.forEach(world => {
2023-04-09 04:47:23 +01:00
world.tick();
2023-04-08 20:52:47 +01:00
});
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}`));
2023-04-08 20:52:47 +01:00
}
sendToAllClients(buffer:Buffer) {
this.clients.forEach(client => {
client.send(buffer);
});
}
2023-08-20 01:18:05 +01:00
sendChatMessage(text:string) {
this.sendToAllClients(new PacketChat(text).writeData());
Console.printInfo(`[CHAT] ${TextColorParser.ParseConsole(text)}`);
2023-08-20 01:18:05 +01:00
}
2023-11-09 21:59:45 +00:00
async handleLoginRequest(reader:IReader, socket:Socket, setMPClient:(mpclient:MPClient) => void) {
2023-04-09 04:47:23 +01:00
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);
2023-04-09 04:47:23 +01:00
if (world instanceof World) {
const clientEntity = new Player(this, world, loginPacket.username);
2023-11-09 21:59:45 +00:00
if (this.saveManager.playerDataOnDisk.includes(clientEntity.username)) {
clientEntity.fromSave(await this.saveManager.readPlayerDataFromDisk(clientEntity.username));
} else {
clientEntity.position.set(8, 70, 8);
}
2023-04-09 04:47:23 +01:00
world.addEntity(clientEntity);
2023-04-10 14:42:14 +01:00
const client = new MPClient(this, socket, clientEntity);
2023-04-09 04:47:23 +01:00
setMPClient(client);
clientEntity.mpClient = client;
this.clients.set(loginPacket.username, client);
2023-08-20 01:18:05 +01:00
this.sendChatMessage(`\u00a7e${loginPacket.username} joined the game`);
2023-04-09 04:47:23 +01:00
socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, dimension).writeData());
2023-04-09 04:47:23 +01:00
socket.write(new PacketSpawnPosition(8, 64, 8).writeData());
2023-11-02 08:31:43 +00:00
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();
2023-04-10 14:42:14 +01:00
world.players.forEach(player => {
if (player.entityId !== clientEntity.entityId && clientEntity.distanceTo(player) < World.ENTITY_MAX_SEND_DISTANCE) {
2023-11-05 10:58:35 +00:00
// Inform the joining player of the players around them
2023-11-02 08:31:43 +00:00
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());
2023-11-05 10:58:35 +00:00
player.sendPlayerEquipment(clientEntity);
// Inform players around the joining player of the joined player
2023-04-10 14:42:14 +01:00
player.mpClient?.send(thisPlayerSpawn);
2023-11-05 10:58:35 +00:00
clientEntity.sendPlayerEquipment(player);
2023-04-10 14:42:14 +01:00
}
});
2023-11-09 21:59:45 +00:00
socket.write(new PacketPlayerPositionLook(clientEntity.position.x, clientEntity.position.y, clientEntity.position.y + 0.62, clientEntity.position.z, 0, 0, false).writeData());
2023-10-29 05:08:26 +00:00
const playerInventory = clientEntity.inventory;
socket.write(new PacketWindowItems(0, playerInventory.getInventorySize(), playerInventory.constructInventoryPayload()).writeData());
2023-04-09 04:47:23 +01:00
} else {
socket.write(new PacketDisconnectKick("Failed to find world to put player in.").writeData());
}
}
handleHandshake(reader:IReader, socket:Socket) {
2023-04-09 04:47:23 +01:00
const handshakePacket = new PacketHandshake().readData(reader);
socket.write(handshakePacket.writeData());
}
2023-04-08 20:52:47 +01:00
onConnection(socket:Socket) {
2023-04-09 04:19:10 +01:00
let mpClient:MPClient;
2023-04-09 04:47:23 +01:00
const setMPClient = (mpclient:MPClient) => {
mpClient = mpclient;
}
2023-04-09 04:19:10 +01:00
const playerDisconnect = (err:Error) => {
mpClient.entity.world.removeEntity(mpClient.entity);
this.clients.remove(mpClient.entity.username);
2023-08-20 01:18:05 +01:00
this.sendChatMessage(`\u00a7e${mpClient.entity.username} left the game`);
2023-04-09 04:47:23 +01:00
if (typeof(err) !== "boolean") {
Console.printError(`Client disconnected with error: ${err.message}`);
}
2023-04-09 04:19:10 +01:00
}
socket.on("close", playerDisconnect.bind(this));
socket.on("error", playerDisconnect.bind(this));
2023-04-08 20:52:47 +01:00
socket.on("data", chunk => {
const reader = createReader(Endian.BE, chunk);
2023-04-08 20:52:47 +01:00
2023-04-09 04:19:10 +01:00
// Let mpClient take over if it exists
if (mpClient instanceof MPClient) {
mpClient.handlePacket(reader);
return;
}
2023-04-08 20:52:47 +01:00
const packetId = reader.readUByte();
switch (packetId) {
2023-04-09 04:47:23 +01:00
// TODO: Handle timeouts at some point, idk.
2023-04-10 21:52:30 +01:00
case Packet.KeepAlive: break;
case Packet.LoginRequest: this.handleLoginRequest(reader, socket, setMPClient.bind(this)); break;
case Packet.Handshake: this.handleHandshake(reader, socket); break;
2023-04-08 20:52:47 +01:00
}
});
}
}