// Node. import msgpack from 'msgpack-lite'; import {performance} from 'perf_hooks'; // 3rd party. // 2nd party. import {EntityPacketSynchronizer} from '@avocado/entity'; import {InputPacket} from '@avocado/input'; import {Synchronizer} from '@avocado/state'; import {Ticker} from '@avocado/timing'; // 1st party. import {WorldTime} from '../common/world-time'; import {createEntityForConnection} from './create-entity-for-connection'; import {createRoom} from './create-server-room'; // Create game. export default class Game { constructor() { const config = this.readConfig(); // Packets. this.entityPacketSynchronizer = new EntityPacketSynchronizer(); // Room. this.room = createRoom(); this.room.world.stepTime = config.simulationInterval; for (const entity of this.room.allEntities()) { this.onEntityAddedToRoom(entity); } this.room.on('entityAdded', this.onEntityAddedToRoom, this); // World time. Start at 10 am for testing. this.worldTime = new WorldTime(); this.worldTime.hour = 10; // Entity tracking. this.informables = []; // State synchronization. this.synchronizer = new Synchronizer({ room: this.room, worldTime: this.worldTime, }); this.informTicker = new Ticker(config.informInterval); this.informTicker.on('tick', this.inform, this); // Simulation. this.mainLoopHandle = setInterval( this.createMainLoop(), 1000 * config.simulationInterval ); } destroy(fn) { clearInterval(this.mainLoopHandle); this.room.destroy(); fn(); } bundleEntityPackets() { const bundledEntityPackets = new Map(); this.entityPacketSynchronizer.flushPackets((packetEntity, packets) => { for (let i = 0; i < this.informables.length; ++i) { const entity = this.informables[i]; if (!entity.seesEntity(packetEntity)) { return; } if (!bundledEntityPackets.has(entity)) { bundledEntityPackets.set(entity, new Map()); } for (let j = 0; j < packets.length; j++) { const packet = packets[j]; const Packet = packet.constructor; if (!bundledEntityPackets.get(entity).has(Packet)) { bundledEntityPackets.get(entity).set(Packet, packet); } else { bundledEntityPackets.get(entity).get(Packet).bundleWith(packet); } } } }); return bundledEntityPackets; } acceptConnection(socket) { // Create and track a new entity for the connection. const entity = createEntityForConnection(socket); // Track informables. this.informables.push(entity); entity.on('destroyed', () => { const index = this.informables.indexOf(entity); if (-1 !== index) { this.informables.splice(index, 1); } }); // Add entity to room. this.room.addEntityToLayer(entity, 'everything'); // Initial information. entity.inform(this.synchronizer.state); // Listen for events. socket.on('packet', this.createPacketListener(socket)); socket.on('disconnect', this.createDisconnectionListener(socket)); } createDisconnectionListener(socket) { const {entity} = socket; return () => { if (entity.is('existent')) { entity.destroy(); } }; } createMainLoop() { let lastTime = performance.now(); return () => { const now = performance.now(); const elapsed = (now - lastTime) / 1000; lastTime = now; // Tick synchronized. this.synchronizer.tick(elapsed); // Tick informer. this.informTicker.tick(elapsed); } } createPacketListener(socket) { const {entity} = socket; return (packet) => { this.entityPacketSynchronizer.acceptPacket(packet); if (packet instanceof InputPacket) { entity.inputState = packet.toState(); } }; } flushEntityPackets() { const bundledEntityPackets = this.bundleEntityPackets(); const entities = Array.from(bundledEntityPackets.keys()); for (let i = 0; i < entities.length; i++) { const entity = entities[i]; const packets = Array.from(bundledEntityPackets.get(entity).values()); for (let j = 0; j < packets.length; j++) { const packet = packets[j]; entity.socket.send(packet); } } } inform() { // Inform entities of the new state. for (let i = 0; i < this.informables.length; ++i) { const entity = this.informables[i]; entity.inform(this.synchronizer.state); } // Flush entity packets. this.flushEntityPackets(); } onEntityAddedToRoom(entity) { this.entityPacketSynchronizer.trackEntity(entity); } readConfig() { return { informInterval: 1 / 60, simulationInterval: 1 / 60, }; } }