creatures
Version:
Library to interface with the Creatures 2 game
584 lines (458 loc) • 13 kB
JavaScript
var child_process = require('child_process'),
libpath = require('path'),
Blast = __Protoblast,
Fn = Blast.Collection.Function,
fs = require('graceful-fs');
// Get the Creatures namespace
var Creatures = Fn.getNamespace('Develry.Creatures');
/**
* The Small Furry Creatures Ole connection class
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.0
*/
var SfcOle = Fn.inherits('Develry.Creatures.Base', function SfcOle() {
// Create the queues
this.createQueue('main');
this.createQueue('checker');
// Connection attempts
this.connection_attempts = 0;
});
/**
* Create a queue
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @return {FunctionQueue}
*/
SfcOle.setMethod(function createQueue(type) {
var queue,
name;
// Construct the queue property name
name = type + '_queue';
// Create a function queue
queue = Fn.createQueue();
// Only 1 command can be processed at a time
queue.limit = 1;
// The queue can start now
queue.start();
// Store the queue
this[name] = queue;
// Create the buffer
this[type + '_buffer'] = '';
return queue;
});
/**
* Spawn something with the given arguments
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {String} path
* @param {Object} arg_object
*
* @return {ChildProcess}
*/
SfcOle.setMethod(function spawn(path, arg_object) {
if (!arg_object) {
arg_object = {};
}
return child_process.spawn(path, [JSON.stringify(arg_object)]);
});
/**
* Create the connection,
* if C2 is not running, it'll be started.
* If instances are already running they will be closed first
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*/
SfcOle.setMethod(function connect() {
var that = this,
vbole_path = libpath.resolve(__dirname, '..', 'vbole.exe');
if (this.vbole_main) {
// Remove all listeners from the existing instances
this.vbole_main.removeAllListeners();
this.vbole_checker.removeAllListeners();
this.log('debug', 'Restarting VBOLE instances');
// Kill the instances
this.vbole_main.kill();
this.vbole_checker.kill();
}
// Create an instance of the vbole application,
// which connects to Creatures 2
this.vbole_main = this.spawn(vbole_path, {do_debug: true});
// Create another instance of the same vbole application,
// but this one will only be used for checking on CAOS errors
this.vbole_checker = this.spawn(vbole_path, {error_dialog_check: false, do_debug: true});
// Listen to the exit code
this.vbole_main.on('exit', function onExit(code) {
that.log('error', 'Main vbole application has closed with code', code, ', attempting reconnection', that.connection_attempts++);
that.connect();
});
this.vbole_main.on('error', function onError(err) {
that.log('error', 'Main:', err);
});
// Listen for stdin errors (writes after close & such)
this.vbole_main.stdin.on('error', function onError(err) {
// Ignore
that.log('error', 'Main:', err);
});
this.vbole_checker.on('exit', function onExit(code) {
that.log('error', 'Checker vbole application has closed with code', code, ', attempting reconnection', that.connection_attempts++);
that.connect();
});
this.vbole_checker.on('error', function onError(err) {
that.log('error', 'Checker error:', err);
});
// Listen to error output
this.vbole_main.stderr.on('data', function onError(data) {
// Convert the buffer to a string
data = String(data);
// Just output debug messages
if (data[0] == '[' && Blast.Bound.String.startsWith(data, '[DEBUG]')) {
that.log('debug', data);
return;
}
// Is the error a JSON object?
if (data[0] == '{') {
data = JSON.parse(data);
// Send this as an error to the callback
if (that.last_main_callback) {
that.last_main_callback(data);
}
// Emit this as an error we need to handle
that.emit('vbole_error', data, function respond(response) {
if (!response) {
response = {};
}
if (!response.type) {
response.type = '';
}
that.vbole_main.stdin.write(JSON.stringify(response));
}, null);
} else {
that.log('error', 'VBOle:', data);
}
});
// Listen to output
this.vbole_main.stdout.on('data', function onData(data) {
that.gotOleData('main', String(data));
});
// Listen to the second application too
this.vbole_checker.stdout.on('data', function onDataTwo(data) {
that.gotOleData('checker', String(data));
});
// Listen to the second application too
this.vbole_checker.stderr.on('data', function onErrorTwo(data) {
// Convert the buffer to a string
data = String(data);
// Just output debug messages
if (data[0] == '[' && Blast.Bound.String.startsWith(data, '[DEBUG]')) {
that.log('debug', '[CHECKER]', data);
return;
}
that.log('error', '[CHECKER] ', data);
});
// Do an initial error dialog check
that.sendCheckerJSON({type: 'checkerrordialog'}, function didInitialCheck(err, data) {
that.log('debug', 'Initial error check:', err, data);
});
// Set the path, because VB6 is too stupid to know it itself
this.sendJSON({
type : 'setpath',
command : libpath.resolve(__dirname, '..')
});
});
/**
* Method to process incoming ole data
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.1
*
* @param {String} type
* @param {String} str
*/
SfcOle.setMethod(function gotOleData(type, str) {
var name = type + '_buffer';
// Add this to the current buffer
this[name] += str;
//console.log(this.buffer);
// See if the buffer ends with \r\n
if (this[name].slice(-2) == '\r\n') {
// It does, so get everything before the returns
str = this[name].slice(0, -2);
// Reset the buffer
this[name] = '';
// Emit as response
this.emit(type + '_response', str);
}
});
/**
* Send a command to the second instance and callback with the response
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Object} json
* @param {Function} callback
*/
SfcOle.setMethod(function sendCheckerJSON(object, callback) {
return this._sendJSON('checker', object, callback);
});
/**
* Send a command to the main vbole instance and callback with the response
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Object} json
* @param {Function} callback
*/
SfcOle.setMethod(function sendJSON(object, callback) {
return this._sendJSON('main', object, callback);
});
/**
* Send a command and callback with the response
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.0
* @version 0.2.7
*
* @param {String} target
* @param {Object} json
* @param {Function} callback
*/
SfcOle.setMethod(function _sendJSON(target, object, callback) {
var that = this,
multiple = Array.isArray(object),
original_callback,
vbole_name,
message,
timeout,
err;
if (!callback) {
callback = Fn.thrower;
}
original_callback = callback;
if (!this.vbole_main) {
this.connect();
}
// Make sure the callback is only called once
callback = Fn.regulate(function doCallback(err) {
that['last_sent_callback_' + target] = null;
original_callback.apply(null, arguments);
}, function overflow(count, args) {
that.log('error', 'VBOLE reply came too late, callback already called:', count, args);
});
// Remember this callback
that['last_' + target + '_callback'] = callback;
vbole_name = 'vbole_' + target;
if (target == 'checker') {
if (this.vbole_checker.send_count > 3) {
timeout = 500;
} else {
if (!this.vbole_checker.send_count) {
this.vbole_checker.send_count = 0;
}
// Initial wait will be at least 1 second
timeout = 3500;
}
this.vbole_checker.send_count++;
} else {
timeout = 4000;
}
// Send 1 command at a time
this[target + '_queue'].add(function queueCommand(done) {
var bomb;
// Make sure "done" gets called only once
done = Fn.regulate(done);
// Create a timebomb that'll execute the callback
// after a certain amount of time
bomb = Fn.timebomb(timeout, function timedout(err) {
var new_error;
// Call done on the next tick
Blast.nextTick(done);
// Create a new error
new_error = new Error('VBOLE "' + target + '" did not respond to request after ' + timeout + 'ms');
// Indicate it's a timeout
new_error.timeout = true;
that.log('error', 'VBOLE "' + target + '" did not respond to request after', timeout, 'ms', target, object);
that.log('error', ' »» Request was ' + JSON.stringify(object));
callback(new_error);
});
// Set the last date we sent
that['last_sent_' + target] = object;
// Listen for the response
that.once(target + '_response', function gotResponse(response) {
// Call done on the next tick
Blast.nextTick(done);
// Defuse the bomb
bomb.defuse();
// The response is always a json object since v0.2.1
response = JSON.parse(response);
// See if some error caused an error
err = extractError(response, object);
if (err) {
callback(err);
} else {
callback(null, response);
}
});
// Stringify the command
message = JSON.stringify(object);
if (!that[vbole_name].stdin.writable) {
// Call done on the next tick
Blast.nextTick(done);
bomb.defuse();
callback(new Error('VBOle input has closed'));
return;
}
try {
// Send it to the executable
that[vbole_name].stdin.write(message);
} catch (err) {
// Call done on the next tick
Blast.nextTick(done);
bomb.defuse();
callback(err);
}
});
});
/**
* Extract an error from the response object/array
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.2
*
* @param {Array|Object} response
* @param {Array|Object} request
*
* @return {Error}
*/
function extractError(response, request) {
var result,
entry,
req,
i;
response = Blast.Bound.Array.cast(response);
request = Blast.Bound.Array.cast(request);
for (i = 0; i < response.length; i++) {
entry = response[i];
if (entry.error) {
req = request[i] || {};
result = new Error('Error in "' + req.type + '" command: ' + entry.error);
result.type = req.type;
result.vbmsg = entry.error;
result.req = req;
break;
}
}
if (result) {
result.original_response = response;
}
return result;
}
/**
* Escape a string that should be typed verbatim
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {String} keys
*
* @return {String}
*/
SfcOle.setMethod(function escapeKeys(keys) {
var result;
if (keys == null) {
throw new Error('Keys parameter should not be null');
}
keys = String(keys);
// Unescaped these characters would perform a special "SendKeys" function.
// For example: an unescaed tilde (~) would result in a return
result = keys.replace(/[\+\^\%\~\(\)\[\]\{\}]/gi, '{$&}');
return result;
});
/**
* Send a CAOS command and callback with the response
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.2
*
* @param {String} caos
* @param {Function} callback
*/
SfcOle.setMethod(function sendCAOS(caos, callback) {
var that = this,
attempt = 0,
payload,
bomb;
// Make sure the callback can only be called 1 time
callback = Fn.regulate(callback || Fn.thrower);
// Listen for timeout
bomb = Fn.timebomb(3000, function exploded() {
that.sendCheckerJSON({type: 'checkerrordialog'}, function gotResult(err, result) {
var new_data;
attempt++;
if (err) {
return callback(err);
}
// No dialogs found? Give it more time then?
if (!result.result) {
if (attempt > 5) {
return callback(new Error('CAOS timeout, no dialog box detected, command was: ' + caos));
}
bomb = Fn.timebomb(3000, exploded);
return;
}
new_data = {
error: 'DialogBox',
elements: result.elements
};
that.emit('vbole_error', new_data, function respond(response) {
if (!response) {
return callback(new Error('Unhandled dialogbox, do it yourself'));
}
if (response.type == 'close') {
that.sendCheckerJSON([
//{type: 'geterrordialog'},
{type: 'close'}
], function done(err, result) {
if (err) {
return callback(err);
}
callback(new Error('Caos command caused an error, code was: ' + caos));
});
}
}, null);
});
});
payload = {
type : 'caos',
command : caos
};
this.sendJSON(payload, function sentJSON(err, result) {
// Defuse the timebomb
bomb.defuse();
if (err) {
return callback(err);
}
if (result.error) {
return callback(new Error(result.error));
}
return callback(null, result.result);
});
});
module.exports = SfcOle;