mudb
Version:
Real-time database for multiplayer games
236 lines (207 loc) • 8.69 kB
text/typescript
import { MuSchema } from './schema/schema';
import { MuWriteStream, MuReadStream } from './stream';
import { MuSocket, MuData } from './socket/socket';
import { MuLogger } from './logger';
import { stableStringify } from './util/stringify';
export type MuAnySchema = MuSchema<any>;
export type MuMessageType = MuAnySchema;
export interface MuAnyMessageTable {
[message:string]:MuMessageType;
}
export interface MuMessageInterface<MessageTable extends MuAnyMessageTable> {
abstractAPI:{
[message in keyof MessageTable]:(event:MessageTable[message]['identity'], unreliable:boolean) => void;
};
userAPI:{
[message in keyof MessageTable]:(event:MessageTable[message]['identity'], unreliable?:boolean) => void;
};
schema:MuSchema<{
type:keyof MessageTable;
data:MessageTable[keyof MessageTable]['identity'];
}>;
serializer:{
[message in keyof MessageTable]:(data:MessageTable['identity']) => string;
};
}
export interface MuProtocolSchema<ClientMessage extends MuAnyMessageTable, ServerMessage extends MuAnyMessageTable> {
name?:string;
client:ClientMessage;
server:ServerMessage;
}
export type MuAnyProtocolSchema = MuProtocolSchema<MuAnyMessageTable, MuAnyMessageTable>;
export type MuBandwidthAccumulator = {
count:number,
bytes:number,
};
export type MuProtocolBandwidth = {
[sessionId:string]:{
sent:{ [message:string]:MuBandwidthAccumulator },
received:{ [message:string]:MuBandwidthAccumulator },
},
};
export class MuMessageFactory {
public schemas:MuAnySchema[];
public messageNames:string[];
public jsonStr:string;
constructor (schema:MuAnyMessageTable, private idBase:number) {
this.messageNames = Object.keys(schema).sort();
this.schemas = new Array(this.messageNames.length);
this.messageNames.forEach((name, id) => {
this.schemas[id] = schema[name];
});
const json = this.schemas.map((s) => s.json);
this.jsonStr = <string>stableStringify(json);
}
public createDispatch (sockets:MuSocket[], bandwidth:MuProtocolBandwidth) {
const result = {};
this.messageNames.forEach((name, messageId) => {
const schema = this.schemas[messageId];
result[name] = (data, unreliable?:boolean) => {
const stream = new MuWriteStream(128);
stream.writeVarint(this.idBase + messageId);
schema.diff(schema.identity, data, stream);
const contentBytes = stream.bytes();
const numBytes = contentBytes.byteLength;
for (let i = 0; i < sockets.length; ++i) {
const socket = sockets[i];
socket.send(contentBytes, unreliable);
if (!bandwidth[socket.sessionId].sent[name]) {
bandwidth[socket.sessionId].sent[name] = {
count: 0,
bytes: 0,
};
}
const acc = bandwidth[socket.sessionId].sent[name];
acc.count += 1;
acc.bytes += numBytes;
}
stream.destroy();
};
});
return result;
}
public createSendRaw (sockets:MuSocket[], bandwidth:MuProtocolBandwidth) {
const rawId = this.idBase + this.messageNames.length;
return function (data:MuData, unreliable?:boolean) {
if (typeof data === 'string') {
const packet = JSON.stringify({
i: rawId,
d: data,
});
const numBytes = packet.length << 1;
for (let i = 0; i < sockets.length; ++i) {
const socket = sockets[i];
socket.send(packet, unreliable);
const acc = bandwidth[socket.sessionId].sent['raw'];
acc.count += 1;
acc.bytes += numBytes;
}
} else {
const size = 5 + data.length;
const stream = new MuWriteStream(size);
stream.writeVarint(rawId);
const { uint8 } = stream.buffer;
uint8.set(data, stream.offset);
stream.offset += data.length;
const bytes = stream.bytes();
const numBytes = bytes.byteLength;
for (let i = 0; i < sockets.length; ++i) {
const socket = sockets[i];
socket.send(bytes, unreliable);
const acc = bandwidth[socket.sessionId].sent['raw'];
acc.count += 1;
acc.bytes += numBytes;
}
stream.destroy();
}
};
}
}
export class MuProtocolFactory {
public protocolFactories:MuMessageFactory[] = [];
public jsonStr:string;
constructor (protocolSchemas:MuAnyMessageTable[]) {
let counter = 0;
for (let i = 0; i < protocolSchemas.length; ++i) {
const factory = new MuMessageFactory(protocolSchemas[i], counter);
this.protocolFactories.push(factory);
counter += factory.messageNames.length + 1;
}
this.jsonStr = this.protocolFactories.map((factory) => factory.jsonStr).join();
}
public createParser(
spec:{
messageHandlers:{ [name:string]:(data, unreliable:boolean) => void },
rawHandler:(data, unreliable:boolean) => void,
}[],
logger:MuLogger,
bandwidth:MuProtocolBandwidth[],
sessionId:string,
) {
// flattened schema and handler tables
const schemaTable:(MuAnySchema|null)[] = [];
const handlerTable:((data, unreliable:boolean) => void)[] = [];
const protocolIdTable:number[] = [];
const messageNameTable:string[] = [];
spec.forEach(({ messageHandlers, rawHandler }, id) => {
const { messageNames, schemas } = this.protocolFactories[id];
for (let i = 0; i < messageNames.length; ++i) {
schemaTable.push(schemas[i]);
handlerTable.push(messageHandlers[messageNames[i]]);
protocolIdTable.push(id);
messageNameTable.push(messageNames[i]);
}
schemaTable.push(null);
handlerTable.push(rawHandler);
protocolIdTable.push(id);
messageNameTable.push('raw');
});
return (data:MuData, unreliable:boolean) => {
if (typeof data === 'string') {
const object = JSON.parse(data);
const id = object.i;
if (id < 0 || id >= handlerTable.length) {
throw new Error(`invalid message id: ${id}`);
}
if ('d' in object) {
handlerTable[id].call(null, object.d, unreliable);
const acc = bandwidth[protocolIdTable[id]][sessionId].received.raw;
acc.count += 1;
acc.bytes += data.length << 1;
}
} else {
const stream = new MuReadStream(data);
const id = stream.readVarint();
if (id < 0 || id >= handlerTable.length) {
throw new Error(`invalid message id: ${id}`);
}
const schema = schemaTable[id];
const handler = handlerTable[id];
const protocolId = protocolIdTable[id];
const messageName = messageNameTable[id];
if (!bandwidth[protocolId][sessionId].received[messageName]) {
bandwidth[protocolId][sessionId].received[messageName] = {
count: 0,
bytes: 0,
};
}
const acc = bandwidth[protocolId][sessionId].received[messageName];
acc.count += 1;
acc.bytes += data.byteLength;
// null schema implies raw handler
if (schema === null) {
handler.call(null, stream.bytes(), unreliable);
return;
}
let msg;
if (stream.offset < stream.length) {
msg = schema.patch(schema.identity, stream);
} else {
msg = schema.clone(schema.identity);
}
handler.call(null, msg, unreliable);
schema.free(msg);
}
};
}
}