2023-04-08 20:52:47 +01:00
|
|
|
import { Config } from "../config";
|
|
|
|
import { Console } from "../console";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { createReader } from "../bufferStuff/index";
|
2023-04-08 20:52:47 +01:00
|
|
|
import { FunkyArray } from "../funkyArray";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { IReader } from "../bufferStuff/readers/IReader";
|
|
|
|
import { Server, Socket, SocketAddress } from "net";
|
2023-04-08 20:52:47 +01:00
|
|
|
import { MPClient } from "./MPClient";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { Packet } from "./enums/Packet";
|
2023-04-08 20:52:47 +01:00
|
|
|
import { PacketKeepAlive } from "./packets/KeepAlive";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { PacketHandshake } from "./packets/Handshake";
|
2023-04-08 20:52:47 +01:00
|
|
|
import { PacketLoginRequest } from "./packets/LoginRequest";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { PacketChat } from "./packets/Chat";
|
2023-04-08 20:52:47 +01:00
|
|
|
import { PacketSpawnPosition } from "./packets/SpawnPosition";
|
|
|
|
import { PacketPlayerPositionLook } from "./packets/PlayerPositionLook";
|
2023-04-10 14:42:14 +01:00
|
|
|
import { PacketNamedEntitySpawn } from "./packets/NamedEntitySpawn";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { PacketDisconnectKick } from "./packets/DisconnectKick";
|
|
|
|
import { Player } from "./entities/Player";
|
2023-04-17 02:05:01 +01:00
|
|
|
import { SaveCompressionType } from "./enums/SaveCompressionType";
|
2023-05-02 10:24:48 +01:00
|
|
|
import { WorldSaveManager } from "./WorldSaveManager";
|
|
|
|
import { World } from "./World";
|
|
|
|
import { Endian } from "../bufferStuff/Endian";
|
2023-04-08 20:52:47 +01:00
|
|
|
|
|
|
|
export 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;
|
2023-04-09 04:47:23 +01:00
|
|
|
private readonly serverClock:NodeJS.Timer;
|
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-04-08 20:52:47 +01:00
|
|
|
private worlds:FunkyArray<number, World>;
|
2023-04-11 07:47:56 +01:00
|
|
|
public saveManager:WorldSaveManager;
|
2023-04-09 04:19:10 +01:00
|
|
|
private overworld:World;
|
|
|
|
|
|
|
|
// 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-04-17 02:05:01 +01:00
|
|
|
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
|
2023-04-11 07:47:56 +01:00
|
|
|
let worldSeed = typeof(this.config.seed) === "string" ? this.hashCode(this.config.seed) : this.config.seed;
|
|
|
|
|
|
|
|
// Init save manager and load seed from it if possible
|
|
|
|
this.saveManager = new WorldSaveManager(this.config, worldSeed);
|
|
|
|
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>();
|
2023-04-11 07:47:56 +01:00
|
|
|
this.worlds.set(0, this.overworld = new World(this.saveManager, worldSeed));
|
2023-04-09 04:19:10 +01:00
|
|
|
|
|
|
|
// Generate spawn area (overworld)
|
2023-04-11 07:47:56 +01:00
|
|
|
(async () => {
|
|
|
|
const generateStartTime = Date.now();
|
|
|
|
Console.printInfo("Generating spawn area...");
|
|
|
|
for (let x = -3; x < 3; x++) {
|
|
|
|
for (let z = -3; z < 3; z++) {
|
|
|
|
await this.overworld.getChunkSafe(x, z);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Console.printInfo(`Done! Took ${Date.now() - generateStartTime}ms`);
|
|
|
|
}).bind(this)();
|
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) {
|
|
|
|
this.clients.forEach(client => {
|
|
|
|
client.send(this.keepalivePacket);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
this.server.listen(config.port, () => Console.printInfo(`Minecraft server started at ${config.port}`));
|
|
|
|
}
|
|
|
|
|
|
|
|
sendToAllClients(buffer:Buffer) {
|
|
|
|
this.clients.forEach(client => {
|
|
|
|
client.send(buffer);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
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 world = this.worlds.get(0);
|
|
|
|
if (world instanceof World) {
|
|
|
|
const clientEntity = new Player(this, world, loginPacket.username);
|
|
|
|
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);
|
|
|
|
|
|
|
|
this.sendToAllClients(new PacketChat(`\u00a7e${loginPacket.username} joined the game`).writeData());
|
|
|
|
|
|
|
|
socket.write(new PacketLoginRequest(clientEntity.entityId, "", 0, 0).writeData());
|
|
|
|
socket.write(new PacketSpawnPosition(8, 64, 8).writeData());
|
|
|
|
|
2023-04-10 14:42:14 +01:00
|
|
|
const thisPlayerSpawn = new PacketNamedEntitySpawn(clientEntity.entityId, clientEntity.username, clientEntity.absX, clientEntity.absY, clientEntity.absZ, clientEntity.absYaw, clientEntity.absPitch, 0).writeData();
|
|
|
|
world.players.forEach(player => {
|
|
|
|
if (player.entityId !== clientEntity.entityId && clientEntity.distanceTo(player) < World.ENTITY_MAX_SEND_DISTANCE) {
|
|
|
|
socket.write(new PacketNamedEntitySpawn(player.entityId, player.username, player.absX, player.absY, player.absZ, player.absYaw, player.absPitch, 0).writeData());
|
|
|
|
player.mpClient?.send(thisPlayerSpawn);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-04-09 04:47:23 +01:00
|
|
|
socket.write(new PacketPlayerPositionLook(8, 70, 70.62, 8, 0, 0, false).writeData());
|
|
|
|
} else {
|
|
|
|
socket.write(new PacketDisconnectKick("Failed to find world to put player in.").writeData());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-02 10:24:48 +01:00
|
|
|
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);
|
|
|
|
this.sendToAllClients(new PacketChat(`\u00a7e${mpClient.entity.username} left the game`).writeData());
|
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 => {
|
2023-05-02 10:24:48 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|