2019-03-20 15:28:18 -05:00
|
|
|
// Node.
|
2019-04-13 18:42:53 -05:00
|
|
|
import msgpack from 'msgpack-lite';
|
2019-03-20 15:28:18 -05:00
|
|
|
import {performance} from 'perf_hooks';
|
|
|
|
// 3rd party.
|
|
|
|
// 2nd party.
|
2019-04-28 22:35:20 -05:00
|
|
|
import {EntityPacketSynchronizer} from '@avocado/entity';
|
2019-04-15 10:03:14 -05:00
|
|
|
import {InputPacket} from '@avocado/input';
|
2019-04-07 20:04:34 -05:00
|
|
|
import {Synchronizer} from '@avocado/state';
|
2019-04-09 09:55:25 -05:00
|
|
|
import {Ticker} from '@avocado/timing';
|
2019-03-27 15:51:58 -05:00
|
|
|
// 1st party.
|
2019-04-04 07:31:21 -05:00
|
|
|
import {WorldTime} from '../common/world-time';
|
2019-03-27 15:51:58 -05:00
|
|
|
import {createEntityForConnection} from './create-entity-for-connection';
|
|
|
|
import {createRoom} from './create-server-room';
|
2019-03-20 15:28:18 -05:00
|
|
|
// Create game.
|
2019-04-20 14:16:06 -05:00
|
|
|
export default class Game {
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
const config = this.readConfig();
|
2019-04-28 22:35:20 -05:00
|
|
|
// Packets.
|
|
|
|
this.entityPacketSynchronizer = new EntityPacketSynchronizer();
|
2019-04-20 14:16:06 -05:00
|
|
|
// Room.
|
|
|
|
this.room = createRoom();
|
2019-05-03 08:49:03 -05:00
|
|
|
this.room.world.stepTime = config.simulationInterval;
|
2019-04-28 22:35:20 -05:00
|
|
|
for (const entity of this.room.allEntities()) {
|
|
|
|
this.onEntityAddedToRoom(entity);
|
|
|
|
}
|
|
|
|
this.room.on('entityAdded', this.onEntityAddedToRoom, this);
|
2019-04-20 14:16:06 -05:00
|
|
|
// 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,
|
|
|
|
});
|
2019-04-22 14:06:57 -05:00
|
|
|
this.informTicker = new Ticker(config.informInterval);
|
|
|
|
this.informTicker.on('tick', this.inform, this);
|
2019-04-20 14:16:06 -05:00
|
|
|
// Simulation.
|
|
|
|
this.mainLoopHandle = setInterval(
|
|
|
|
this.createMainLoop(),
|
|
|
|
1000 * config.simulationInterval
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-04-22 14:17:54 -05:00
|
|
|
destroy(fn) {
|
|
|
|
clearInterval(this.mainLoopHandle);
|
|
|
|
this.room.destroy();
|
|
|
|
fn();
|
|
|
|
}
|
|
|
|
|
2019-04-28 22:35:20 -05:00
|
|
|
bundleEntityPackets() {
|
|
|
|
const bundledEntityPackets = new Map();
|
2019-05-04 12:43:11 -05:00
|
|
|
const flushed = this.entityPacketSynchronizer.flushPackets();
|
2019-05-04 13:07:01 -05:00
|
|
|
for (let i = 0; i < this.informables.length; ++i) {
|
|
|
|
const entity = this.informables[i];
|
|
|
|
// Get a list of all visible entities for this informable.
|
|
|
|
const areaToInform = entity.areaToInform;
|
|
|
|
const room = entity.room;
|
|
|
|
const visibleEntities = room.visibleEntities(areaToInform);
|
|
|
|
const it = flushed.entries();
|
|
|
|
for (let value = it.next(); !value.done; value = it.next()) {
|
|
|
|
const packetEntity = value.value[0];
|
|
|
|
if (-1 === visibleEntities.indexOf(packetEntity)) {
|
2019-05-04 12:43:11 -05:00
|
|
|
continue;
|
2019-04-28 22:35:20 -05:00
|
|
|
}
|
|
|
|
if (!bundledEntityPackets.has(entity)) {
|
|
|
|
bundledEntityPackets.set(entity, new Map());
|
|
|
|
}
|
2019-05-04 13:07:01 -05:00
|
|
|
const packets = value.value[1];
|
2019-05-04 12:43:11 -05:00
|
|
|
const it2 = packets.values();
|
|
|
|
for (let value2 = it2.next(); !value2.done; value2 = it2.next()) {
|
|
|
|
const packet = value2.value;
|
2019-04-28 22:35:20 -05:00
|
|
|
const Packet = packet.constructor;
|
|
|
|
if (!bundledEntityPackets.get(entity).has(Packet)) {
|
|
|
|
bundledEntityPackets.get(entity).set(Packet, packet);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
bundledEntityPackets.get(entity).get(Packet).bundleWith(packet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-05-04 12:43:11 -05:00
|
|
|
}
|
2019-04-28 22:35:20 -05:00
|
|
|
return bundledEntityPackets;
|
|
|
|
}
|
|
|
|
|
2019-04-20 14:16:06 -05:00
|
|
|
acceptConnection(socket) {
|
2019-03-20 15:28:18 -05:00
|
|
|
// Create and track a new entity for the connection.
|
2019-03-21 00:59:00 -05:00
|
|
|
const entity = createEntityForConnection(socket);
|
2019-03-27 15:51:58 -05:00
|
|
|
// Track informables.
|
2019-04-20 14:16:06 -05:00
|
|
|
this.informables.push(entity);
|
2019-03-27 15:51:58 -05:00
|
|
|
entity.on('destroyed', () => {
|
2019-04-20 14:16:06 -05:00
|
|
|
const index = this.informables.indexOf(entity);
|
2019-03-27 15:51:58 -05:00
|
|
|
if (-1 !== index) {
|
2019-04-20 14:16:06 -05:00
|
|
|
this.informables.splice(index, 1);
|
2019-03-27 15:51:58 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
// Add entity to room.
|
2019-04-20 14:16:06 -05:00
|
|
|
this.room.addEntityToLayer(entity, 'everything');
|
2019-04-08 08:01:17 -05:00
|
|
|
// Initial information.
|
2019-04-20 14:16:06 -05:00
|
|
|
entity.inform(this.synchronizer.state);
|
2019-03-20 15:28:18 -05:00
|
|
|
// Listen for events.
|
2019-04-20 14:16:06 -05:00
|
|
|
socket.on('packet', this.createPacketListener(socket));
|
|
|
|
socket.on('disconnect', this.createDisconnectionListener(socket));
|
2019-03-20 15:28:18 -05:00
|
|
|
}
|
2019-04-20 14:16:06 -05:00
|
|
|
|
2019-04-22 14:17:54 -05:00
|
|
|
createDisconnectionListener(socket) {
|
|
|
|
const {entity} = socket;
|
|
|
|
return () => {
|
|
|
|
if (entity.is('existent')) {
|
|
|
|
entity.destroy();
|
|
|
|
}
|
|
|
|
};
|
2019-04-20 14:16:06 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
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) => {
|
2019-04-28 22:35:20 -05:00
|
|
|
this.entityPacketSynchronizer.acceptPacket(packet);
|
2019-04-20 14:16:06 -05:00
|
|
|
if (packet instanceof InputPacket) {
|
|
|
|
entity.inputState = packet.toState();
|
|
|
|
}
|
|
|
|
};
|
2019-04-09 09:55:25 -05:00
|
|
|
}
|
2019-04-20 14:16:06 -05:00
|
|
|
|
2019-04-28 22:35:20 -05:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-22 14:06:57 -05:00
|
|
|
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);
|
|
|
|
}
|
2019-04-28 22:35:20 -05:00
|
|
|
// Flush entity packets.
|
|
|
|
this.flushEntityPackets();
|
|
|
|
}
|
|
|
|
|
|
|
|
onEntityAddedToRoom(entity) {
|
|
|
|
this.entityPacketSynchronizer.trackEntity(entity);
|
2019-04-22 14:06:57 -05:00
|
|
|
}
|
|
|
|
|
2019-04-20 14:16:06 -05:00
|
|
|
readConfig() {
|
|
|
|
return {
|
2019-04-23 16:59:17 -05:00
|
|
|
informInterval: 1 / 60,
|
|
|
|
simulationInterval: 1 / 60,
|
2019-04-20 14:16:06 -05:00
|
|
|
};
|
2019-03-20 15:28:18 -05:00
|
|
|
}
|
2019-04-20 14:16:06 -05:00
|
|
|
|
2019-03-20 15:28:18 -05:00
|
|
|
}
|