acsp
Version:
Node.JS implementation of the Assetto Corsa Server Protocol
417 lines (359 loc) • 13.2 kB
JavaScript
var dgram = require('dgram');
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('acsp');
var BufferReader = require('buffer-reader');
var BufferWriter = require('buffer-write');
var _ = require('lodash')
var Promise = require('bluebird');
var PromiseQueue = require('promise-queue');
PromiseQueue.configure(Promise);
/**
* Create a "wide" string from a regular JS string
* @param {string} str
* @return {Buffer}
*/
function StringW(str) {
if (typeof str !== "string") {
// Ensure we're operating on a string.
str = "" + str;
}
if (str.length > 255) {
// Sorry, "wide" strings can only be up to 255 characters;
// The remainder will be truncated.
str = str.substr(0, 255);
}
var buf = new Buffer((str.length * 4) + 1);
buf.writeUInt8(str.length, 0);
if (str.length > 0) {
// Hacky method that ignores half the UTF-32 space
buf.write(str.split('').join('\u0000') + '\u0000', 1, str.length * 4, 'utf-16le');
}
return buf;
}
/**
* ACSP Constructor
* @param {object} options
*/
function ACSP(options) {
var self = this;
this.options = options;
this.sock = dgram.createSocket(this.options.sockType);
Promise.promisifyAll(this.sock);
// Message send queue
this.sendQueue = new PromiseQueue(1, Infinity);
this.sock.on('error', function(err){
// pass socket errors to main
self.emit('error', err);
});
this.sock.on('message', function(msg, rinfo){
//debug('MESSAGE', msg, rinfo);
self._handleMessage(new BufferReader(msg), rinfo);
});
this.sock.bind(12000);
}
// allow this prototype to emit events
ACSP.prototype.__proto__ = EventEmitter.prototype;
// define some constants
ACSP.NEW_SESSION = 50;
ACSP.NEW_CONNECTION = 51;
ACSP.CONNECTION_CLOSED = 52;
ACSP.CAR_UPDATE = 53;
ACSP.CAR_INFO = 54; // Sent as response to ACSP_GET_CAR_INFO command
ACSP.END_SESSION = 55;
ACSP.VERSION = 56;
ACSP.CHAT = 57;
ACSP.CLIENT_LOADED = 58;
ACSP.SESSION_INFO = 59;
ACSP.ERROR = 60;
ACSP.LAP_COMPLETED = 73;
// EVENTS
ACSP.CLIENT_EVENT = 130;
// EVENT TYPES
ACSP.CE_COLLISION_WITH_CAR = 10;
ACSP.CE_COLLISION_WITH_ENV = 11;
// COMMANDS
ACSP.REALTIMEPOS_INTERVAL = 200;
ACSP.GET_CAR_INFO = 201;
ACSP.SEND_CHAT = 202; // Sends chat to one car
ACSP.BROADCAST_CHAT = 203; // Sends chat to everybody
ACSP.GET_SESSION_INFO = 204;
ACSP.SET_SESSION_INFO = 205;
ACSP.KICK_USER = 206;
// ACSP.VERSION = 0; // Not implemented
ACSP.prototype.getCarInfo = function(carId){
var buf = BufferWriter()
.writeUInt8(ACSP.GET_CAR_INFO)
.writeUInt8(carId)
.toBuffer();
this._send(buf);
var self = this;
var handler;
return new Promise(function(resolve, reject){
handler = function(car_info){
if (car_info.car_id === carId) {
resolve(car_info);
self.removeListener('car_info', handler);
}
};
self.on('car_info', handler);
}).timeout(1000).finally(function(){
self.removeListener('car_info', handler);
});
}
ACSP.prototype.getSessionInfo = function(sess_index){
// sess_index is optional
if (typeof sess_index === 'undefined') {
sess_index = -1;
}
var buf = BufferWriter()
.writeUInt8(ACSP.GET_SESSION_INFO)
.writeInt16LE(sess_index)
.toBuffer();
this._send(buf);
var self = this;
var handler;
return new Promise(function(resolve, reject){
handler = function(session_info){
if (session_info.sess_index === sess_index
|| (sess_index === -1 && session_info.sess_index === session_info.current_session_index)) {
resolve(session_info);
self.removeListener('session_info', handler);
}
};
self.on('session_info', handler);
}).timeout(1000).finally(function(){
self.removeListener('session_info', handler);
});
}
ACSP.prototype.setSessionInfo = function(sessioninfo){
var buf = BufferWriter()
.writeUInt8(ACSP.SET_SESSION_INFO)
.writeUInt8(sessioninfo.sess_index) // Session Index
.write(StringW(sessioninfo.name)) // Session Name
.writeUInt8(sessioninfo.type) // Session type
.writeUInt32LE(sessioninfo.laps) // Laps
.writeUInt32LE(sessioninfo.time) // Time (in seconds)
.writeUInt32LE(sessioninfo.wait_time) // Wait time (in seconds)
.toBuffer();
return this._send(buf);
}
ACSP.prototype.enableRealtimeReport = function(interval){
var buf = BufferWriter()
.writeUInt8(ACSP.REALTIMEPOS_INTERVAL)
.writeUInt16LE(interval)
.toBuffer();
return this._send(buf);
}
ACSP.prototype.sendChat = function(carid, message){
var buf = BufferWriter()
.writeUInt8(ACSP.SEND_CHAT)
.writeUInt8(carid)
.write(StringW(message))
.toBuffer();
return this._send(buf);
}
ACSP.prototype.broadcastChat = function(message){
var buf = BufferWriter()
.writeUInt8(ACSP.BROADCAST_CHAT)
.write(StringW(message))
.toBuffer();
return this._send(buf);
}
ACSP.prototype.kickUser = function(car_id){
var buf = new BufferWriter()
.writeUInt8(ACSP.KICK_USER)
.writeUInt8(car_id)
.toBuffer();
return this._send(buf);
}
ACSP.prototype.getVersion = function(){
return this.getSessionInfo().then(function(info){
return info.version;
});
}
/**
* [private] Send packet to AC server
* @param {Buffer} buf Contents of the message
* @return {Promise} resolved when message is sent
*/
ACSP.prototype._send = function(buf) {
var self = this;
//debug('buflen', buf.length);
return this.sendQueue.add(function(){
return self.sock.sendAsync(buf, 0, buf.length, self.options.port, self.options.host);
});
};
/**
* [private] Parse an incoming message and emit appropriate events
* @param {Buffer} msg Message content
* @param {object} rinfo Information about the sender
* @return {undefined}
*/
ACSP.prototype._handleMessage = function(msg, rinfo) {
var packet_id = msg.nextUInt8();
switch (packet_id) {
case ACSP.CHAT:
this.emit('chat_message',{
car_id: msg.nextUInt8(),
message: this.readStringW(msg)
});
break;
case ACSP.CLIENT_LOADED:
var car_id = msg.nextUInt8();
this.emit('client_loaded',car_id);
break;
case ACSP.VERSION:
// TODO: Do something with this??
var version = msg.nextUInt8();
this.emit('version', version);
break;
case ACSP.NEW_SESSION:
case ACSP.SESSION_INFO:
var session_info = {
version: msg.nextUInt8(),
sess_index: msg.nextUInt8(),
current_session_index: msg.nextUInt8(),
session_count: msg.nextUInt8(),
server_name: this.readStringW(msg),
track: this.readString(msg),
track_config: this.readString(msg),
name: this.readString(msg),
type: msg.nextUInt8(),
time: msg.nextUInt16LE(),
laps: msg.nextUInt16LE(),
wait_time: msg.nextUInt16LE(),
ambient_temp: msg.nextUInt8(),
road_temp: msg.nextUInt8(),
weather_graphics: this.readString(msg),
elapsed_ms: msg.nextInt32LE()
};
this.emit('version',session_info.version);
this.emit('session_info',session_info);
if(packet_id == ACSP.NEW_SESSION){ this.emit('new_session',session_info);}
break;
case ACSP.END_SESSION:
debug('end session packet!');
this.emit('end_session',{
filename: this.readStringW(msg)
});
break;
case ACSP.CLIENT_EVENT:
var client_event = {
ev_type: msg.nextUInt8(),
car_id: msg.nextUInt8(),
}
if(client_event.ev_type == ACSP.CE_COLLISION_WITH_CAR){
client_event.other_car_id = msg.nextUInt8();
}
_.extend(client_event, {
speed: msg.nextFloatLE(),
world_pos: this.readVector3f(msg),
rel_pos: this.readVector3f(msg)
})
this.emit('client_event',client_event);
if (client_event.ev_type == ACSP.CE_COLLISION_WITH_ENV){
this.emit('collide_env',client_event);
} else if(client_event.ev_type == ACSP.CE_COLLISION_WITH_CAR) {
this.emit('collide_car',client_event);
}
break;
case ACSP.CAR_INFO:
this.emit('car_info', {
car_id: msg.nextUInt8(),
is_connected: msg.nextUInt8(),
car_model: this.readStringW(msg),
car_skin: this.readStringW(msg),
driver_name: this.readStringW(msg),
driver_team: this.readStringW(msg),
driver_guid: this.readStringW(msg)
});
break;
case ACSP.CAR_UPDATE:
this.emit('car_update',{
car_id: msg.nextUInt8(),
pos: this.readVector3f(msg),
velocity: this.readVector3f(msg),
gear: msg.nextUInt8(),
engine_rpm: msg.nextUInt16LE(),
normalized_spline_pos: msg.nextFloatLE()
});
break;
case ACSP.NEW_CONNECTION:
var conn_info = {
driver_name: this.readStringW(msg),
driver_guid: this.readStringW(msg),
car_id: msg.nextUInt8(),
car_model: this.readString(msg),
car_skin: this.readString(msg)
};
this.emit('new_connection', conn_info);
// var self = this;
// this.pollUntilStatusKnown(conn_info.car_id).then(function(isConnected){
// if (isConnected) {
// self.emit('is_connected', conn_info.car_id);
// } else {
// self.emit('connection_closed', conn_info);
// }
// });
break;
case ACSP.CONNECTION_CLOSED:
this.emit('connection_closed',{
driver_name: this.readStringW(msg),
driver_guid: this.readStringW(msg),
car_id: msg.nextUInt8(),
car_model: this.readString(msg),
car_skin: this.readString(msg)
});
break;
case ACSP.LAP_COMPLETED:
var lapinfo = {
car_id: msg.nextUInt8(),
laptime: msg.nextUInt32LE(),
cuts: msg.nextUInt8(),
cars_count: msg.nextUInt8()
};
lapinfo.leaderboard = [];
for (var i = 0; i < lapinfo.cars_count; i++) {
lapinfo.leaderboard.push({
rcar_id: msg.nextUInt8(),
rtime: msg.nextUInt32LE(),
rlaps: msg.nextUInt8()
})
}
lapinfo.grip_level = msg.nextFloatLE();
this.emit('lap_completed', lapinfo)
break;
case ACSP.ERROR:
debug('ERROR', 'MSG:', this.readStringW(msg));
break;
default:
debug('Unrecognised message', packet_id, 'MSG:', msg);
break;
}
};
ACSP.prototype.readString = function(buf) {
var length = buf.nextUInt8();
var strBuf = buf.nextBuffer(length);
return strBuf.toString('utf8');
}
ACSP.prototype.readStringW = function(buf) {
var length = buf.nextUInt8();
var strBuf = buf.nextBuffer(length*4);
return strBuf.toString('utf-16le').split('\u0000').join('');
}
ACSP.prototype.readVector3f = function (buf){
return {
x: buf.nextFloatLE(),
y: buf.nextFloatLE(),
z: buf.nextFloatLE()
};
}
/**
* Close the ACSP socket and try not to cause any memory leaks
* @return {undefined}
*/
ACSP.prototype.close = function(){
this.socket.removeAllListeners();
this.socket.unref();
};
module.exports = ACSP;