creatures
Version:
Library to interface with the Creatures 2 game
2,251 lines (1,789 loc) • 45.5 kB
JavaScript
var child_process = require('child_process'),
enabled_graceful_cleanup,
menu_map = require('./menu_map.js'),
libpath = require('path'),
Blast = __Protoblast,
names = require('./names.js'),
Obj = Blast.Bound.Object,
cid = 0,
tmp = require('tmp'),
Fn = Blast.Collection.Function,
fs = require('graceful-fs'),
os = require('os');
// Get the Creatures namespace
var Creatures = Fn.getNamespace('Develry.Creatures');
/**
* The Creatures Application class
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*/
var App = Fn.inherits('Develry.Creatures.Base', function CreaturesApplication() {
// Create a connection object
this.ole = new Creatures.SfcOle();
// All Creature instances by moniker
this.creatures = {};
// All Creature instances *in the game* by their id
this.creatures_by_id = {};
// Egg objects
this.eggs = {};
// For now, only c2 is supported
this.is_c1 = false;
// The name of the currentl open world
this.world_name = '';
// The getting-creatures queue
this.creatures_queue = Function.createQueue({limit: 1, enabled: true});
// Listen for VBOle error objects
this.listenForVBErrors();
if (process.platform == 'win32') {
// Get the path to the process
this.getProcessPath();
// Get the play state
this.getIsPlaying();
}
// Play/pause state
this.paused = null;
});
/**
* Export Protoblast for nw.js
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.0
* @version 0.2.0
*/
App.setProperty('__Protoblast', Blast);
/**
* All available names
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.1
* @version 0.1.1
*/
App.setProperty('names', names);
/**
* Possible locations of executable
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.3
* @version 0.2.3
*/
App.setProperty('exe_locations', [
'C:\\GOG Games\\Creatures 2\\creatures2.exe',
'C:\\Program Files\\Creatures Albian Years\\Creatures 2\\creatures2.exe',
'C:\\Program Files (x86)\\Creatures Albian Years\\Creatures 2\\creatures2.exe',
]);
/**
* Prepare the path to the user's "My Documents" folder
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*/
App.prepareProperty(function my_documents_path() {
var personal_folder,
release,
path;
// Get the os release version.
// We always assume it's windows
release = os.release();
// Detect XP or 2K
if (release.indexOf('5.1') == 0 || release.indexOf('5.0') == 0) {
personal_folder = 'My Documents';
} else {
personal_folder = 'Documents';
}
// Construct the path
path = libpath.resolve(process.env.USERPROFILE, personal_folder);
return path;
});
/**
* Prepare the path to the directory containing the worlds data
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*/
App.prepareProperty(function worlds_data_path() {
var my_documents = this.my_documents_path,
game_name,
path;
if (this.is_c1) {
game_name = 'Creatures 1';
} else {
game_name = 'Creatures 2';
}
path = libpath.resolve(my_documents, 'Creatures', game_name);
return path;
});
/**
* Prepare the path to the directory containg all worlds history
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*/
App.prepareProperty(function worlds_history_path() {
return libpath.resolve(this.worlds_data_path, 'History');
});
/**
* The known class names
*
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*/
App.setProperty('classification_system', [
{
name : 'Invisible',
genus : []
},
{
name : 'Simple objects',
genus : [
'System',
'Call button',
'Nature',
'Good Plant',
'Creature Egg',
'Processed Food',
'Drink',
'Food dispensor',
'Implements',
'Cliff Edge',
'Detritus',
'Cures',
'Toys',
'Weather',
'Bad Plant',
'Animal Nest',
'Bad Bug',
'Bug',
'Bad Critter',
'Critter',
'Seeds',
'Leaf',
'Root Vegetables',
'Flowers',
'Fruit'
]
},
{
name : 'Compount objects',
genus : [
'Movers',
'Lifts',
'Computers',
'Fun',
'Messages',
'LeftRight',
'Incubators',
'Teleporters',
'<Empty>',
'Machines'
]
},
{
name : 'Creatures',
genus : [
'Norn',
'Grendel',
'Ettin',
'Geat'
]
}
]);
/**
* The amount of creatures in the world
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*
* @type {Number}
*/
App.setProperty(function creature_count() {
return Blast.Bound.Object.size(this.creatures_by_id);
});
/**
* Serialize something for use in LStrings
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*
* @param {Object}
*
* @return {String}
*/
App.setMethod(function serialize(obj) {
var result = '',
keys = Object.keys(obj),
key,
i;
for (i = 0; i < keys.length; i++) {
key = keys[i];
if (result) {
result += ' ';
}
result += '{' + key + ' ' + obj[key] + '}';
}
return result;
});
/**
* Parse a serialized object
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*
* @param {String}
*
* @return {Object}
*/
App.setMethod(function parse(str) {
var rx = /\{(\w+)\s*(.*?)\}/g,
result = {},
match;
while (match = rx.exec(str)) {
result[match[1]] = match[2];
}
return result;
});
/**
* Find the process path
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.3
* @version 0.2.3
*
* @return {String}
*/
App.setMethod(function _findProcessPath() {
var path,
i;
for (i = 0; i < this.exe_locations.length; i++) {
path = this.exe_locations[i];
if (fs.existsSync(path)) {
return path;
}
}
});
/**
* Get the path to the current C2 process
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.0
* @version 0.2.3
*
* @param {Function} callback
*/
App.setCacheMethod(function getProcessPath(callback) {
var that = this,
retries = 0;
if (!callback) {
callback = Function.thrower;
}
Fn.parallel(function getLanguage(next) {
that.getLanguage(function gotLanguage(err) {
if (err) {
console.warn('Some problem with getting language: ', err);
}
next();
});
}, function getProcessPath(next) {
that.ole.sendJSON([
// Make the c2window the active window
{type: 'c2window'},
// And get the process path
{type: 'getprocesspath'}
], function gotResponses(err, res) {
if (err || !res[0] || !res[0].handle) {
console.error('Could not get C2 Window', err, res);
retries++;
// Check some probable process paths
if (retries > 3) {
that.process_path = that._findProcessPath();
}
if (!that.process_path) {
setTimeout(function doRetry() {
getProcessPath.call(that, callback);
}, 1000);
return;
}
} else {
// 0 will be the C2 window handle, 1 is the process
that.process_path = res[1].process_path;
}
// Also store the dir
that.process_dir = libpath.dirname(that.process_path);
that.emitOnce('process_path', that.process_path);
next();
});
}, function done(err) {
if (err) {
return callback(err);
}
// Emit the ready event
that.emitOnce('ready');
callback(null, that.process_path);
});
});
/**
* Get the language of this windows installation
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.0
* @version 0.2.2
*
* @param {Function} callback
*/
App.setCacheMethod(function getLanguage(callback) {
var that = this;
if (!callback) {
callback = Function.thrower;
}
this.ole.sendJSON([
{type: 'language'},
], function gotResponses(err, res) {
var code;
// Assume english
if (err || !res || !res[0] || !res[0].language) {
console.warn('Error getting language, falling back to english', err, res);
that.language = 'en';
return callback(null, that.language);
}
code = Number(res[0].language);
switch (code) {
case 0x13:
that.language = 'nl';
break;
case 0x0c:
that.language = 'fr';
break;
case 0x07:
that.language = 'de';
break;
case 0x10:
that.language = 'it';
break;
case 0x0a:
that.language = 'es';
break;
case 0x11:
that.language = 'jp';
break;
default:
that.language = 'en';
}
callback(null, that.language);
});
});
/**
* Get the locale of this windows installation
* No longer used, getting language via LANG command is preferred
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.2
* @version 0.2.2
*/
App.setMethod(function getSystemLocale() {
var stdout,
result;
if (this.locale) {
return this.locale;
}
// Run the script to get the locale
stdout = child_process.execSync(libpath.resolve(__dirname, '..', 'windows-locale.cmd'));
// Trim the result
result = stdout.toString().trim();
if (result.indexOf('ECHO') > -1) {
result = null;
return;
}
this.locale = result;
// Also get the language
this.language = this.locale.split('-')[0] || this.locale;
return this.locale;
});
/**
* Listen for vb errors
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*/
App.setCacheMethod(function listenForVBErrors() {
var that = this,
last_error = null;
// Listen for vbole_error events,
// these require a response object!
this.ole.on('vbole_error', function gotError(data, callback) {
if (data.error == 'DialogBox') {
// Let the library implementers handle it.
// Albian Command has a setting that can automatically close it
that.emit('error_dialogbox', data, callback, last_error, null);
// A possible way of solving it is to just close it, like:
// callback({type: 'close'});
} else if (data.error == 'CrashDialog') {
// Let the library implementers handle it.
// Albian Command has a setting that can automatically close it
that.emit('error_crashdialog', data, callback, last_error, null);
} else {
that.emit('error_vbole', data, callback, last_error, null);
}
last_error = data;
});
});
/**
* Get a temporary file
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Object} options
* @param {Function} callback
*/
App.setMethod(function getTemporaryFile(options, callback) {
if (typeof options == 'function') {
callback = options;
options = {};
}
if (options.mode == null) {
options.mode = 0644;
}
if (options.discardDescriptor == null) {
// Don't give us a file descriptor, this causes the file to be "open"
// and then Creatures 2 will refuse to open it
options.discardDescriptor = true;
}
tmp.file(options, function gotFile(err, path) {
if (err) {
return callback(err);
}
callback(null, path);
});
if (!enabled_graceful_cleanup) {
enabled_graceful_cleanup = true;
tmp.setGracefulCleanup();
}
});
/**
* A copyFile implementation
* (Because fs.copyFile is only available in node v8.5+)
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {String} source
* @param {String} target
* @param {Function} callback
*/
App.setMethod(function copyFile(source, target, callback) {
var that = this,
read_stream,
write_stream;
if (!callback) {
callback = Fn.thrower;
}
// Allow the callback to only be called once
callback = Fn.regulate(callback);
read_stream = fs.createReadStream(source);
read_stream.on('error', function onError(err) {
that.error('error', 'Read stream error in file', source, ':', err);
callback(err);
});
write_stream = fs.createWriteStream(target);
write_stream.on('error', function onError(err) {
that.error('error', 'Write stream error in file', source, ':', err);
callback(err);
});
write_stream.on('close', function done(ex) {
callback();
});
read_stream.pipe(write_stream);
});
/**
* Guess the name of the current open world
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*
* @param {Array} creatures Optional creatures to use for check
* @param {Function} callback
*/
App.setMethod(function getWorldName(creatures, callback) {
var that = this,
age,
worlds = {},
new_name = false;
if (typeof creatures == 'function') {
callback = creatures;
creatures = null;
}
// See if we found the world name already
if (this.world_name && this._world_name_time) {
// Calculate the "age" of the found name
age = Date.now() - this._world_name_time;
// We cache this for 60 seconds
if (age < 60 * 1000) {
return Blast.nextTick(callback, null, null, this.world_name);
}
}
if (this.hasBeenSeen('getting_world_name')) {
return this.once('_got_world_name', callback);
}
this.emit('getting_world_name');
Fn.series(function getCreatures(next) {
if (creatures) {
return next();
}
// Get all creatures
that.getCreatures(function gotCreatures(err, _creatures) {
if (err) {
return next(err);
}
creatures = _creatures;
next();
});
}, function checkCreatures(next) {
var tasks = [];
creatures.forEach(function eachCreature(creature) {
tasks.push(function getHistory(next) {
creature.getWorldNames(function gotNames(err, names) {
var name,
i;
if (err) {
return next(err);
}
for (i = 0; i < names.length; i++) {
name = names[i];
if (!worlds[name]) {
worlds[name] = 0;
}
worlds[name]++;
}
next();
});
});
});
Fn.parallel(tasks, next);
}, function done(err) {
var count = 0,
name,
val,
key;
that.unsee('getting_world_name');
if (err) {
callback(err);
return that.emit('_got_world_name', err);
}
for (key in worlds) {
val = worlds[key];
if (val > count) {
count = val;
name = key;
}
}
if (name) {
if (that.world_name != name) {
// Indicate this is a new name
// (without there being an older name)
if (that.world_name) {
new_name = true;
}
that.world_name = name;
that.emit('world_name', name, new_name);
}
}
// Remember last time we found the world name
that._world_name_time = Date.now();
callback(null, name, new_name);
that.emit('_got_world_name', null, name, new_name);
});
});
/**
* Get all existing world names (worlds that have a History folder)
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*
* @param {Function} callback
*/
App.setMethod(function getWorlds(callback) {
var that = this,
age,
tasks = [],
result = [],
result_map = {},
history_path = this.worlds_history_path;
// See if we found the world name already
if (this._worlds_array && this._worlds_time) {
// Calculate the "age" of the found name
age = Date.now() - this._worlds_time;
// We cache this for 60 seconds
if (age < 60 * 1000) {
return Blast.nextTick(callback, null, null, this._worlds_array, this._worlds_object);
}
}
if (this.hasBeenSeen('getting_worlds')) {
return this.once('got_worlds', function gotWorlds(result, result_map) {
callback(null, result, result_map);
});
}
this.emit('getting_worlds');
// The "History" folder actually also holds world info for World.sfc
result_map['World.sfc'] = history_path;
fs.readdir(history_path, function gotContents(err, list) {
if (err) {
return callback(err);
}
list.forEach(function eachFile(file) {
tasks.push(function getStat(next) {
var full_path = libpath.resolve(history_path, file, 'GameLog');
// Stat the gamelog file
fs.stat(full_path, function gotStat(err, stat) {
if (!err && stat.isFile()) {
// Push the name of the world to the array
result.push(file);
// Add it to the map, too
result_map[file] = libpath.resolve(history_path, file);
}
next();
});
});
});
Fn.parallel(tasks, function done(err) {
if (err) {
return callback(err);
}
// Remember when we last got this list
that._worlds_time = Date.now();
// Store the list & object
that._worlds_array = result;
that._worlds_object = result_map;
that.unsee('getting_worlds');
callback(null, result, result_map);
that.emit('got_worlds', result, result_map);
});
});
});
/**
* Get the history files for the given world
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {String} world_name The name of the world to get for (optional)
* @param {Function} callback
*/
App.setMethod(function getWorldHistory(world_name, callback) {
var that = this,
result = [];
if (typeof world_name == 'function') {
callback = world_name;
world_name = null;
}
Fn.series(function getWorld(next) {
if (world_name) {
return next();
}
that.getWorldName(function gotName(err, name) {
if (name) {
world_name = name;
next();
} else {
that.getWorlds(function gotWorldNames(err, worlds, world_map) {
world_name = Object.keys(world_map);
next();
});
}
});
}, function getWorlds(next) {
var tasks = [];
world_name = Blast.Collection.Array.cast(world_name);
world_name.forEach(function eachWorld(name) {
tasks.push(function getWorldHistory(next) {
var path = libpath.resolve(that.worlds_history_path, name),
subtasks = [];
fs.readdir(path, function gotFiles(err, files) {
files.forEach(function eachFile(file) {
// Only load in creatures history files
if (file.slice(0, 3) != 'cr_') {
return;
}
subtasks.push(function loadFile(next) {
that.readFile(libpath.resolve(path, file), function gotFile(err, buffer) {
var history;
if (err) {
// Ignore errors
return next();
}
// Create the new history object
history = new Creatures.CrHistory(that);
// Process the buffer
history.processBuffer(buffer);
result.push(history);
next();
});
});
});
Fn.parallel(subtasks, next);
});
});
});
Fn.parallel(tasks, next);
}, function done(err) {
if (err) {
return callback(err);
}
callback(null, result);
});
});
/**
* Get the current world save
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*/
App.setMethod(function getWorld(callback) {
var that = this;
if (!callback) {
callback = Fn.thrower;
}
if (!this._sfc_world_cache) {
this._sfc_world_cache = {};
}
this.getWorldName(function gotWorld(err, world_name) {
var world_path,
world_instance;
if (err) {
return callback(err);
}
if (!that._sfc_world_cache[world_name]) {
// Resolve the path to the world file
world_path = libpath.resolve(that.my_documents_path, 'Creatures', 'Creatures 2', world_name + '.sfc');
// Create the new instance
that._sfc_world_cache[world_name] = new Creatures.SfcWorld(that, world_path);
}
world_instance = that._sfc_world_cache[world_name];
world_instance.load(function loaded(err) {
if (err) {
return callback(err);
}
callback(null, world_instance);
});
});
});
/**
* Create a creature instance
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*/
App.setMethod(function createCreatureInstance() {
var creature = new Creatures.Creature(this);
return creature;
});
/**
* Look for an existing creature instance by id or moniker
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*
* @param {String} id_or_moniker The id or moniker of the creature
*
* @return {Creature}
*/
App.setMethod(function getCreatureInstance(id_or_moniker) {
var creature;
// Look by id first
creature = this.creatures_by_id[id_or_moniker];
if (!creature) {
creature = this.creatures[id_or_moniker];
}
return creature;
});
/**
* Get a creature by looking through the loaded creatures first.
* If they're not there, search through the history files
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*
* @param {Boolean} in_world Make sure it's an in-world creature? [true]
* @param {String} id_or_moniker The id or moniker of the creature
* @param {Function} callback
*/
App.setMethod(function getCreature(in_world, id_or_moniker, callback) {
var that = this,
attempts = 1,
creature,
timeout,
temp,
id;
if (typeof in_world != 'boolean') {
callback = id_or_moniker;
id_or_moniker = in_world;
in_world = true;
}
Fn.series(function checkExisting(next) {
// Try getting an existing creature instance
creature = that.getCreatureInstance(id_or_moniker);
next();
}, function checkUpdates(next) {
if (!creature) {
that.getCreatures(false, function done(err, creatures) {
if (err) {
return next(err);
}
// Try getting it like this again
creature = that.getCreatureInstance(id_or_moniker);
next();
});
} else {
next();
}
}, function done(err) {
if (err) {
that.log('error', 'Failed to get creature ' + id_or_moniker + ':', err);
return callback(err);
}
if (creature) {
return callback(null, creature);
}
if (!creature) {
attempts++;
timeout = null;
// If we're looking for a creature in the current world,
// make a few attempts
if (in_world) {
if (attempts > 3) {
return callback(new Error('Could not find creature in world with identifier ' + id_or_moniker))
} else {
timeout = 500;
}
}
if (timeout) {
that.log('debug', 'Could not yet find', id_or_moniker, ', trying attempt', attempts);
return setTimeout(function() {
that.getCreatures(done);
}, timeout);
}
}
// Create a new creature instance if we haven't found anything
creature = that.createCreatureInstance();
that.log('debug', 'Created new Creature instance for', id_or_moniker, 'counter:', that.debugCounter('getCreatureCreate'));
// Load & store by moniker
creature.loadByMoniker(id_or_moniker, function loaded(err) {
if (err) {
return callback(err);
}
callback(null, creature);
});
});
});
/**
* Get an egg
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.1
* @version 0.1.1
*/
App.setMethod(function getEgg(id_or_moniker, callback) {
var that = this,
result;
this.getEggs(function gotEggs(err, eggs) {
if (err) {
return callback(err);
}
eggs.forEach(function eachEgg(egg) {
if (egg.id == id_or_moniker || egg.moniker == id_or_moniker) {
result = egg;
}
});
callback(null, result);
});
});
/**
* Send a CAOS command and callback with the response
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.3
*
* @param {String} str
* @param {Function} callback
*/
App.setAfterMethod('ready', function command(str, callback) {
this.ole.sendCAOS(str, callback);
});
/**
* Set the speed of the game
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.0
* @version 0.2.3
*
* @param {Number} acceleration
* @param {Function} callback
*/
App.setAfterMethod('ready', function setSpeed(acceleration, callback) {
this.ole.sendJSON({
type : 'setspeed',
acceleration : acceleration,
sleeptime : 5
}, callback);
});
/**
* Get the speed of the game (from the speedhack window)
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.6
* @version 0.2.6
*
* @param {Function} callback
*/
App.setAfterMethod('ready', function getSpeed(callback) {
this.ole.sendJSON({
type : 'getspeed',
}, callback);
});
/**
* Get the coordinates of the hand
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {Function} callback
*/
App.setMethod(function getHandPosition(callback) {
this.command('targ pntr,dde: putv post,dde: putv posr,dde: putv posb,dde: putv posl', function gotResponse(err, response) {
var pieces,
result;
if (err) {
return callback(err);
}
pieces = response.split('|');
result = {
top : Number(pieces[0]),
right : Number(pieces[1]),
bottom : Number(pieces[2]),
left : Number(pieces[3])
};
callback(null, result);
});
});
/**
* Unselect creature
* This will, unfortunately, set the window title only to "Creatures 2"
* It won't include the world name
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {Function} callback
*/
App.setMethod(function unselect(callback) {
this.command('setv norn 0', callback);
});
/**
* Take a picture of the currently selected creature
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {Integer} width
* @param {Integer} height
* @param {Function} callback
*/
App.setMethod(function createPicture(width, height, callback) {
var cmd;
// C1 compatible, will always create "temp.spr" or "temp.s16" file
cmd = 'dde: pict ' + String.fromCharCode(width) + '|' + String.fromCharCode(height);
// C2 alternative
//inst,dde: pic2 50 50 [temp.s16],endm
// Create a picture and get the filename, which is always temp.s16
this.command(cmd, function gotResponse(err, filename) {
if (err) {
return callback(err);
}
if (filename.indexOf('000') == 0) {
return callback(new Error('No creature seems to be selected'));
}
callback(null, filename);
});
});
/**
* Format hexadecimal moniker
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {String} hex_moniker
*/
App.setMethod(function formatMoniker(hex_moniker) {
var result = '',
i;
if (Buffer.isBuffer(hex_moniker)) {
hex_moniker = hex_moniker.toString('hex');
} else if (typeof hex_moniker == 'number') {
hex_moniker = hex_moniker.toString(16);
}
// Normalize the moniker
for (i = 0; i < 4; i++) {
result = result + String.fromCharCode(parseInt(hex_moniker.substr(i * 2, 2), 16));
}
return result;
});
/**
* Get all the in-world creature ids
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.4
*
* @param {Function} callback
*/
App.setMethod(function getCreatureIds(callback) {
var that = this;
this.command('inst,enum 4 0 0,dde: putv targ,next,endm', function gotResponse(err, response) {
var pieces;
if (err) {
return callback(err);
}
if (!response.length) {
return callback(null, []);
}
pieces = response.split('|');
callback(null, pieces);
});
});
/**
* Load an export file
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {String|Buffer|Export} filepath
* @param {Function} callback
*/
App.setMethod(function loadExport(filepath, callback) {
var that = this,
creature_exp = new Creatures.Export(this, filepath);
// Load the file and callback with the instance
creature_exp.load(function loaded(err) {
if (err) {
return callback(err);
}
callback(null, creature_exp);
});
return creature_exp;
});
/**
* Unpause the game if needed and restore state afterwards
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.5
*
* @param {Function} task
* @param {Function} callback
*/
App.setMethod(function doUnpaused(task, callback) {
var that = this,
was_paused = this.paused,
task_response;
if (!callback) {
callback = Fn.thrower;
}
Fn.series(function getIsPlaying(next) {
that.getIsPlaying(function gotIsPlaying(err, result) {
if (err) {
return next();
}
that.paused = !result;
next();
});
}, function resumeGame(next) {
that.play(function gotResponse(err) {
// Ignore errors?
next();
});
}, function doTask(next) {
task.call(that, function finishedTask(err, response) {
if (err) {
return next(err);
}
task_response = response;
next();
});
}, function done(err) {
if (was_paused === true) {
// Pause the game again
that.pause(function madePaused(paused_err) {
// Ignore errors?
callback(err, task_response);
});
} else {
callback(err, task_response);
}
});
});
/**
* Import a creature, even if it's already in the world
* (If it is, it gets a new moniker)
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.6
*
* @param {String|Buffer|Export} filepath
* @param {Function} callback
*/
App.setMethod(function importCreature(filepath, callback) {
var that = this,
export_instance,
previous_state = this.paused,
copied_path,
buffer;
if (!callback) {
callback = Function.thrower;
}
if (typeof filepath == 'object') {
if (Buffer.isBuffer(filepath)) {
buffer = filepath;
} else {
export_instance = buffer;
buffer = filepath.buffer;
}
if (!buffer) {
return Blast.setImmediate(function noBuffer() {
callback(new Error('Invalid buffer'));
});
}
}
Fn.series(function resumeGame(next) {
that.play(function gotResponse(err) {
// Ignore errors?
next();
});
}, function doesFileExist(next) {
if (buffer) {
return next();
}
fs.stat(filepath, function gotStat(err, stat) {
if (err) {
return next(err);
}
if (!stat.isFile()) {
return callback(new Error('Given path is not a file'));
}
next();
});
}, function copyFile(next) {
// Create a temporary file with a .exp extension
that.getTemporaryFile({postfix: '.exp'}, function gotTempFile(err, path, fd) {
if (err) {
return next(err);
}
copied_path = path;
if (buffer) {
fs.writeFile(path, buffer, next);
} else {
// Actually copy the original file to the temporary one
that.copyFile(filepath, copied_path, next);
}
});
}, function doImport(next) {
// Escape special characters in the path
var escaped_path = that.ole.escapeKeys(copied_path);
// Send the import commands
that.ole.sendJSON([
{type: 'c2window'},
{type: 'sleep', command: 50},
{type: 'keys', command: '%' + that.menuKey('file') + '{DOWN}' + that.menuKey('import')},
{type: 'sleep', command: 50},
{type: 'window', command: that.menuKey('import_title'), retries: 10},
{type: 'keys', command: escaped_path + '{ENTER}'},
{type: 'sleep', command: 500}
], function doneImport(err) {
var new_error;
if (!err) {
return next();
}
if (err.error == 'CrashDialog') {
new_error = new Error('Importing this creature crashed the game!');
new_error.crashed = true;
new_error.data = err;
if (export_instance) {
export_instance.emit('import_error', new_error);
}
return next(new_error);
}
next(err);
});
}, function done(err) {
if (err) {
return restoreState(err);
}
that.emit('imported_creature', filepath);
restoreState(null);
});
// Function to restate paused state
function restoreState(top_err) {
if (previous_state === true) {
that.pause(function madePaused(err) {
// Ignore errors?
callback(top_err);
});
} else {
callback(top_err);
}
}
});
/**
* Get all the creatures in the currently loaded world
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*
* @param {Boolean} update Update the creatures? [true]
* @param {Function} callback
*/
App.setAfterMethod('ready', function getCreatures(update, callback) {
'use strict';
var that = this,
disappearance_count = 0,
current_ids;
if (typeof update == 'function') {
callback = update;
update = null;
}
if (update == null) {
update = true;
}
if (!update && this.hasBeenSeen('getting_creatures')) {
this.log('debug', 'Letting new non-update Creatures list request wait for an earlier request to finish');
return this.once('_got_creatures', callback);
}
// Emit the getting_creatures event,
// preventing others from calling this function while we're running
this.emit('getting_creatures');
this.creatures_queue.add(function doGetCreatures(next) {
that._gcw = 'series';
// Get all the creature ids currently in the world
Fn.series(function getCreatureIds(next) {
that._gcw = 'creature_ids';
that.getCreatureIds(function gotIds(err, ids) {
var tasks = [];
if (err) {
return next(err);
}
current_ids = ids;
// Iterate over all the in-world ids
ids.forEach(function eachId(id) {
var creature,
is_new;
// Get a reference to the creature object
if (that.creatures_by_id[id]) {
creature = that.creatures_by_id[id];
} else {
// Create the creature instance if there is no creature with
// this id yet
creature = that.createCreatureInstance();
creature.id = id;
is_new = true;
// If no update is needed, return now
if (!update) {
return;
}
}
// Let's make sure this creature is who we think it is.
// Because IDs get re-used in multiple worlds
// So if we opened a new world,
// 2 different creatures might have the same id
if (!is_new && creature.hex_moniker) {
tasks.push(function isSameCreature(next) {
creature.command('dde: getb monk', function gotMoniker(err, response) {
if (err) {
return next(err);
}
// The monikers match, so continue
if (creature.hex_moniker == response) {
return next();
}
// Monikers don't match, this is another creature!
disappearance_count++;
creature.emit('removed', 'disappeared');
// Create a new creature instance
creature = that.createCreatureInstance();
creature.id = id;
is_new = true;
next();
});
});
}
});
// Execute all the checks
Fn.series(tasks, next);
});
}, function checkRemovedCreatures(next) {
var creature,
id;
that._gcw = 'check_removed_creatures';
// Iterate over all the ids in the instance
for (id in that.creatures_by_id) {
creature = that.creatures_by_id[id];
// Do nothing it the creature has already been nullified
if (!creature) {
continue;
}
if (current_ids.indexOf(id) == -1) {
disappearance_count++;
creature.emit('removed', 'disappeared');
that.log('disappeared_creature', 'Creature', creature.moniker, 'has disappeared');
delete that.creatures_by_id[id];
}
}
// If more than 1 creature disappeared, see if we're still in the same world
if (disappearance_count > 1) {
that.log('disappearances', 'During this update', disappearance_count, 'creatures have disappeared, checking for new world');
let creatures = [];
for (let id in that.creatures_by_id) {
creatures.push(that.creatures_by_id[id]);
}
that.getWorldName(creatures, function done(err, name, is_new_name) {
if (err) {
console.warn('Error getting worldname after creature disappearance:', err);
}
if (is_new_name) {
that.log('new_world_name', 'New world name found:', name);
}
next();
});
} else {
next();
}
}, function updateCreatures(next) {
that._gcw = 'actually_update';
// If no udpates are needed, goto next early
if (!update) {
return next();
}
let tasks = [];
for (let id in that.creatures_by_id) {
let creature = that.creatures_by_id[id];
let counter = tasks.length + 1;
// Add a new task
tasks.push(function updateCreature(next) {
var start = Date.now();
creature.update(function done(err) {
if (err) {
return next(err);
}
next(null);
});
});
}
that.log('update_creatures', 'Going to update', tasks.length, 'creatures');
// Execute all the update tasks
Fn.series(tasks, function doneTasks(err) {
if (err) {
return next(err);
}
next(null);
});
}, done);
var bomb = Function.timebomb(10000, function onTimeout(err) {
that.log('error', 'Timeout getting creatures list from creatures2.exe');
try {
done(new Error('Getting creatures timeout after 10 seconds, with update set to ' + update));
} catch (err) {
next();
}
});
// Function that'll call next on the queue & the callback
function done(err) {
that._gcw = 'done_get_creatures';
bomb.defuse();
// Unsee the 'getting_creatures' event
that.unsee('getting_creatures');
if (err) {
that.log('error', 'Error getting creatures: ' + err)
callback(err);
} else {
let result = [];
for (let id in that.creatures_by_id) {
if (that.creatures_by_id[id]) {
result.push(that.creatures_by_id[id]);
}
}
callback(null, result);
that.emit('_got_creatures', err, result);
}
next();
}
});
});
/**
* Get all the eggs
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.1.0
* @version 0.2.7
*
* @param {Function} callback
*/
App.setAfterMethod('ready', function getEggs(callback) {
var that = this,
result = [];
if (this.hasBeenSeen('getting_eggs')) {
return this.once('_got_eggs', callback);
}
// Get all the egg ids
this.command('dde: putv targ,enum 2 5 2,dde: putv targ,next', function gotEggs(err, response) {
var egg_targs,
tasks = [],
stat;
if (err) {
return done(err);
}
// Get the first 4 characters of the response
stat = response.slice(0, 4);
if (stat == 'enum') {
return done(new Error('Failed to get eggs: got enum'));
} else if (stat == 'erro') {
return done(new Error('Failed to get eggs: got ' + stat));
}
// Get all the egg targs ids
egg_targs = response.split('|');
// Iterate over all the egg ids
egg_targs.forEach(function eachEgg(egg_targ) {
if (egg_targ == 0) {
return;
}
tasks.push(function getEgg(next) {
var egg = that.eggs[egg_targ];
// Egg is falsy, probably one of those zero-byte moniker eggs
if (egg === false) {
return next();
}
if (!egg) {
egg = new Creatures.Egg(that, egg_targ);
}
// Remember this egg
that.eggs[egg_targ] = egg;
egg.update(function updatedEgg(err) {
if (err) {
return next(err);
}
if (!egg.moniker) {
delete that.eggs[egg_targ];
return next();
}
// Make sure it really is an egg
result.push(egg);
return next();
});
});
});
// Update all the found eggs
Fn.parallel(tasks, function _done(err) {
if (err) {
that.log('error', 'Error updating eggs: ' + err);
return done(err);
}
// Iterate over all the previously found eggs
Obj.each(that.eggs, function eachEgg(egg, key) {
if (!egg) {
return;
}
// The egg has dissappeared?
if (result.indexOf(egg) == -1) {
// Remove the egg from the egg cache
delete that.eggs[egg.id];
that.log('egg_disappearance', 'Egg', egg.moniker, 'has disappeared');
// And "solve" the disappearance (events)
egg.solveDisappearance();
}
});
done(err, result);
});
});
function done(err, result) {
that.unsee('getting_eggs');
if (err) {
return callback(err);
}
callback(null, result);
that.emit('_got_eggs', result);
}
});
/**
* Return the number of norns above which eggs should stop hatching
* This is probably higher than the hatchery & import limit
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @param {Function} callback
*/
App.setMethod(function getEggLimit(callback) {
var that = this;
this.command('inst,dde: putv eggl,endm', function gotResponse(err, result) {
if (err) {
return callback(err);
}
callback(null, Number(result));
});
});
/**
* Return the number of norns above which the hatchery & import
* functionality is disabled
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @param {Function} callback
*/
App.setMethod(function getHatcheryLimit(callback) {
var that = this;
this.command('inst,dde: putv hatl,endm', function gotResponse(err, result) {
if (err) {
return callback(err);
}
callback(null, Number(result));
});
});
/**
* Get the state of the game
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Function} callback
*/
App.setMethod(function getIsPlaying(callback) {
var that = this;
if (!callback) {
callback = Fn.thrower;
}
// Actually get the "paused" status
this.command('inst,dde: putv paus,endm', function gotResponse(err, result) {
if (err) {
return callback(err);
}
if (result == 0) {
that.paused = false;
} else {
that.paused = true;
}
callback(null, that.paused);
});
});
/**
* Resume the game by pushing on the "Play" button
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Function} callback
*/
App.setMethod(['resume', 'play'], function resume(callback) {
var that = this,
previous = this.paused;
if (!callback) {
callback = Fn.thrower;
}
// Indicate nothing is paused
this.paused = false;
// Send the play commands
this.ole.sendJSON({type: 'play'}, function gotResponse(err, result) {
if (err) {
that.paused = previous;
return callback(err);
}
callback(null);
});
});
/**
* Pause the game by pushing on the "Pause" button
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.1
* @version 0.2.1
*
* @param {Function} callback
*/
App.setMethod(function pause(callback) {
var that = this,
previous = this.paused;
if (!callback) {
callback = Fn.thrower;
}
// Indicate nothing is paused
this.paused = true;
// Send the pause commands
this.ole.sendJSON({type: 'pause'}, function gotResponse(err) {
if (err) {
that.paused = previous;
return callback(err);
}
callback(null);
});
});
/**
* Get the correct alt key for the current windows language
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.2
* @version 0.2.2
*
* @param {String} type
* @param {String} def
*
* @return {String} The correct sequence
*/
App.setMethod(function menuKey(type, def) {
var result,
entry;
entry = menu_map[type];
if (entry) {
result = entry[this.language];
}
if (!result) {
result = def;
}
// Fallback to english if possible
if (!result && entry) {
result = entry['en'];
}
return result;
});
/**
* Save the game (blueberry4$ required)
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @return {Function} callback
*/
App.setAfterMethod('ready', function saveGame(callback) {
// Send the key commands
this.ole.sendJSON([
{type: 'c2window'},
{type: 'sleep', command: 100},
{type: 'keys', command: '%' + this.menuKey('world') + '{DOWN}' + this.menuKey('save')},
{type: 'sleep', command: 50}
], callback);
});
/**
* Get game flow state
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @param {Number} category
* @param {Number} variable
* @return {Function} callback
*/
App.setAfterMethod('ready', function getFlowState(category, variable, callback) {
var that = this,
code = 'dde: putv game ' + category + ' ' + variable;
// Execute the command
this.command(code, function gotState(err, result) {
if (err) {
return callback(err);
}
if (!result || result == '0') {
callback(null, false);
} else {
callback(null, true);
}
});
});
/**
* Set game flow state
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @param {Number} category
* @param {Number} variable
* @param {Number} value
* @return {Function} callback
*/
App.setAfterMethod('ready', function setFlowState(category, variable, value, callback) {
var that = this,
code = 'setv game ' + category + ' ' + variable + ' ' + value;
// Execute the command
this.command(code, function gotState(err, result) {
if (err) {
return callback(err);
}
callback();
});
});
/**
* Enable all the C2 powerups kit
*
* @author Jelle De Loecker <jelle@develry.be>
* @since 0.2.4
* @version 0.2.4
*
* @return {Function} callback
*/
App.setAfterMethod('ready', function enablePowerups(callback) {
var that = this,
code;
if (!callback) {
callback = Fn.thrower;
}
// Enable science kit
code = 'setv game 0 5 1,';
// Enable advanced science kit
code += 'setv game 1 5 1,';
// Enable neuroscience kit
code += 'setv game 0 6 1,';
// Enable scrolling
code += 'setv game 2 1 1,';
// Enable managing ettins & grendels
code += 'setv game 2 0 1';
// Actually get the "paused" status
this.command(code, function gotResponse(err, result) {
if (err) {
return callback(err);
}
if (result == 0) {
that.paused = false;
} else {
that.paused = true;
}
callback(null, that.paused);
});
});
module.exports = App;