/*!
* DecafMUD v0.9.0
* http://decafmud.kicks-ass.net
*
* Copyright 2010, Stendec
*/
/**
* @fileOverview DecafMUD's Core
* @author Stendec
* @version 0.9.0
*/
// Extend the String prototype with endsWith and substr_count.
if ( String.prototype.endsWith === undefined ) {
/** Determine if a string ends with the given suffix.
* @example
* if ( "some string".endsWith("ing") ) {
* // Something Here!
* }
* @param {String} suffix The suffix to test.
* @returns {boolean} true if the string ends with the given suffix */
String.prototype.endsWith = function(suffix) {
var startPos = this.length - suffix.length;
return startPos < 0 ? false : this.lastIndexOf(suffix, startPos) === startPos;
}
}
if ( String.prototype.substr_count === undefined ) {
/** Count the number of times a specific string occures within a larger
* string.
* @example
* "This is a test of a fishy function for string counting.".substr_count("i");
* // Returns: 6
* @param {String} needle The text to search for.
* @returns {Number} The number of matches found. */
String.prototype.substr_count = function(needle) {
var count = 0,
i = this.indexOf(needle);
while ( i !== -1 ) {
count++;
i = this.indexOf(needle, i+1);
}
return count;
}
}
// Extend Array with indexOf if it doesn't exist, for IE8
if ( Array.prototype.indexOf === undefined ) {
Array.prototype.indexOf = function(text,i) {
if ( i === undefined ) { i = 0; }
for(;iAn array with references to all the created instances of DecafMUD.
* Generally, each DecafMUD's id is the instance's index in
* this array.
* @type DecafMUD[] */
DecafMUD.instances = [];
/** The ID of the latest instance of DecafMUD.
* @type number */
DecafMUD.last_id = -1;
/** DecafMUD's version. This can be used to check plugin compatability.
* @example
* if ( DecafMUD.version.major >= 1 ) {
* // Some Code Here
* }
* @example
* alert("You're using DecafMUD v" + DecafMUD.version.toString() + "!");
* // You're using DecafMUD v0.9.0alpha!
* @type Object */
DecafMUD.version = {major: 0, minor: 9, micro: 0, flag: 'alpha',
toString: function(){ return this.major+'.'+this.minor+'.'+this.micro+( this.flag ? '-' + this.flag : ''); } };
// Default Values
DecafMUD.prototype.loaded = false;
DecafMUD.prototype.connecting = false;
DecafMUD.prototype.connected = false;
DecafMUD.prototype.loadTimer = null;
DecafMUD.prototype.timer = null;
DecafMUD.prototype.connect_try = 0;
DecafMUD.prototype.required = 0;
///////////////////////////////////////////////////////////////////////////////
// Plugins System
///////////////////////////////////////////////////////////////////////////////
/** This object stores all the available plugins for DecafMUD using a simple
* hierarchy. Every plugin should register itself in this tree once it's done
* loading.
* @example
* // Add the plugin MyPluginClass to DecafMUD as my_plugin.
* DecafMUD.plugins.Extra.my_plugin = MyPluginClass;
* @namespace All the available plugins for {@link DecafMUD}, in one easy-to-access
tree. */
DecafMUD.plugins = {
/** These plugins provide support for MUD output.
* @type Object */
Display : {},
/** These plugins provide support for different text encodings.
* @type Object */
Encoding : {},
/** These plugins don't fit into any other categories.
* @type Object */
Extra : {},
/** These plugins provide user interfaces for the client.
* @type Object */
Interface : {},
/** These plugins provide translations to other languages.
* @type Object */
Language : {},
/** These plugins provide sockets for network connectivity, a must for a
* mud client.
* @type Object */
Socket : {},
/** These plugins provide persistent storage for the client, letting the
* client remember user settings across browser sessions.
* @type Object */
Storage : {},
/** These plugins provide extra telnet options for adding more sophisticated
* client/server interaction to DecafMUD.
* @type Object */
Telopt : {}
};
/** This plugin handles conversion between raw data and iso-8859-1 encoded
* text, somewhat unimpressively as they're effectively the same thing.
* @type Object */
/** This provides support for iso-8859-1 encoded data to DecafMUD, which isn't
* saying much as you realize that iso-8859-1 is simple, unencoded binary
* strings. We just have this so that the encoding system can work with a
* default encoder.
* @example
* alert(DecafMUD.plugins.Encoding.iso88591.decode("This is some text!"));
* @namespace DecafMUD Character Encoding: iso-8859-1 */
DecafMUD.plugins.Encoding.iso88591 = {
proper : 'ISO-8859-1',
/** Convert iso-8859-1 encoded text to unicode, by doing nothing.
* @example
* DecafMUD.plugins.Encoding.iso88591.decode("\xE2\x96\x93");
* // Becomes: "\xE2\x96\x93"
* @param {String} data The text to decode. */
decode : function(data) { return [data,'']; },
/** Convert unicode characters to iso-8859-1 encoded text, by doing
* nothing. Should probably add some sanity checks in later, but I
* don't really care for now.
* @example
* DecafMUD.plugins.Encoding.iso88591.encode("\xE2\x96\x93");
* // Becomes: "\xE2\x96\x93"
* @param {String} data The text to encode. */
encode : function(data) { return data; }
};
/** This provides support for UTF-8 encoded data to DecafMUD, using built-in
* functions in a slightly hack-ish way to convert between UTF-8 and unicode.
* @example
* alert(DecafMUD.plugins.Encoding.utf8.decode("This is some text!"));
* @namespace DecafMUD Character Encoding: UTF-8 */
DecafMUD.plugins.Encoding.utf8 = {
proper : 'UTF-8',
/** Convert UTF-8 sequences to unicode characters.
* @example
* DecafMUD.plugins.Encoding.utf8.decode("\xE2\x96\x93");
* // Becomes: "\u2593"
* @param {String} data The text to decode. */
decode : function(data) {
try { return [decodeURIComponent( escape( data ) ), '']; }
catch(err) {
// Decode manually so we can catch what's left.
var out = '', i=0, l=data.length,
c = c2 = c3 = c4 = 0;
while ( i < l ) {
c = data.charCodeAt(i++);
if ( c < 0x80) {
// Normal Character
out += String.fromCharCode(c); }
else if ( (c > 0xBF) && (c < 0xE0) ) {
// Two-Byte Sequence
if ( i+1 >= l ) { break; }
out += String.fromCharCode(((c & 31) << 6) | (data.charCodeAt(i++) & 63)); }
else if ( (c > 0xDF) && (c < 0xF0) ) {
// Three-Byte Sequence
if ( i+2 >= l ) { break; }
out += String.fromCharCode(((c & 15) << 12) | ((data.charCodeAt(i++) & 63) << 6) | (data.charCodeAt(i++) & 63)); }
else if ( (c > 0xEF) && (c < 0xF5) ) {
// Four-Byte Sequence
if ( i+3 >= l ) { break; }
out += String.fromCharCode(((c & 10) << 18) | ((data.charCodeAt(i++) & 63) << 12) | ((data.charCodeAt(i++) & 63) << 6) | (data.charCodeAt(i++) & 63)); }
else {
// Bad Character.
out += String.fromCharCode(c); }
}
return [out, data.substr(i)];
} },
/** Encode unicode characters into UTF-8 sequences.
* @example
* DecafMUD.plugins.Encoding.utf8.encode("\u2593");
* // Becomes: "\xE2\x96\x93"
* @param {String} data The text to encode. */
encode : function(data) {
try { return unescape( encodeURIComponent( data ) ); }
catch(err) {
console.dir(err); return data; } }
};
/** The variable storing instances of plugins is called loaded_plugs to avoid
* any unnecessary confusion created by {@link DecafMUD.plugins}.
* @type Object */
DecafMUD.prototype.loaded_plugs = {};
// Create a function for class inheritence
var inherit = function(subclass, superclass) {
var f = function() {};
f.prototype = superclass.prototype;
subclass.prototype = new f();
subclass.superclass = superclass.prototype;
if ( superclass.prototype.constructor == Object.prototype.constructor ) {
superclass.prototype.constructor = superclass; }
};
///////////////////////////////////////////////////////////////////////////////
// TELNET Internals
///////////////////////////////////////////////////////////////////////////////
// Extra Constants
DecafMUD.ESC = "\x1B";
DecafMUD.BEL = "\x07";
// TELNET Constants
DecafMUD.TN = {
// Negotiation Bytes
IAC : "\xFF", // 255
DONT : "\xFE", // 254
DO : "\xFD", // 253
WONT : "\xFC", // 252
WILL : "\xFB", // 251
SB : "\xFA", // 250
SE : "\xF0", // 240
IS : "\x00", // 0
// END-OF-RECORD Marker / GO-AHEAD
EORc : "\xEF", // 239
GA : "\xF9", // 249
// TELNET Options
BINARY : "\x00", // 0
ECHO : "\x01", // 1
SUPGA : "\x03", // 3
STATUS : "\x05", // 5
SENDLOC : "\x17", // 23
TTYPE : "\x18", // 24
EOR : "\x19", // 25
NAWS : "\x1F", // 31
TSPEED : "\x20", // 32
RFLOW : "\x21", // 33
LINEMODE : "\x22", // 34
AUTH : "\x23", // 35
NEWENV : "\x27", // 39
CHARSET : "\x2A", // 42
MSDP : "E", // 69
MSSP : "F", // 70
COMPRESS : "U", // 85
COMPRESSv2 : "V", // 86
MSP : "Z", // 90
MXP : "[", // 91
ZMP : "]", // 93
CONQUEST : "^", // 94
ATCP : "\xC8", // 200
GMCP : "\xC9", // 201
}
var t = DecafMUD.TN;
var iacToWord = function(c) {
var t = DecafMUD.TN;
switch(c) {
case t.IAC : return 'IAC';
case t.DONT : return 'DONT';
case t.DO : return 'DO';
case t.WONT : return 'WONT';
case t.WILL : return 'WILL';
case t.SB : return 'SB';
case t.SE : return 'SE';
case t.BINARY : return 'TRANSMIT-BINARY';
case t.ECHO : return 'ECHO';
case t.SUPGA : return 'SUPPRESS-GO-AHEAD';
case t.STATUS : return 'STATUS';
case t.SENDLOC : return 'SEND-LOCATION';
case t.TTYPE : return 'TERMINAL-TYPE';
case t.EOR : return 'END-OF-RECORD';
case t.NAWS : return 'NEGOTIATE-ABOUT-WINDOW-SIZE';
case t.TSPEED : return 'TERMINAL-SPEED';
case t.RFLOW : return 'REMOTE-FLOW-CONTROL';
case t.AUTH : return 'AUTH';
case t.LINEMODE : return 'LINEMODE';
case t.NEWENV : return 'NEW-ENVIRON';
case t.CHARSET : return 'CHARSET';
case t.MSDP : return 'MSDP';
case t.MSSP : return 'MSSP';
case t.COMPRESS : return 'COMPRESS';
case t.COMPRESSv2 : return 'COMPRESSv2';
case t.MSP : return 'MSP';
case t.MXP : return 'MXP';
case t.ZMP : return 'ZMP';
case t.CONQUEST : return 'CONQUEST-PROPRIETARY';
case t.ATCP : return 'ATCP';
case t.GMCP : return 'GMCP';
}
c = c.charCodeAt(0);
if ( c > 15 ) { return c.toString(16); }
else { return '0' + c.toString(16); }
}
/** Convert a telnet IAC sequence from raw bytes to a human readable format that
* can be output for debugging purposes.
* @example
* var IAC = "\xFF", DO = "\xFD", TTYPE = "\x18";
* DecafMUD.debugIAC(IAC + DO + TTYPE);
* // Returns: "IAC DO TERMINAL-TYPE"
* @param {String} seq The sequence to convert.
* @returns {String} The human readable description of the IAC sequence. */
DecafMUD.debugIAC = function(seq) {
var out = '', t = DecafMUD.TN, state = 0, st = false, l = seq.length,
i2w = iacToWord;
for( var i = 0; i < l; i++ ) {
var c = seq.charAt(i),
cc = c.charCodeAt(0);
// TTYPE Sequence
if ( state === 2 ) {
if ( c === t.ECHO ) { out += 'SEND '; }
else if ( c === t.IS ) { out += 'IS '; }
else if ( c === t.IAC ) {
if ( st ) { st = false; out += '" IAC '; }
else { out += 'IAC '; }
state = 0;
} else {
if ( !st ) { st = true; out += '"'; }
out += c;
}
continue;
}
// MSSP / MSDP Sequence
else if ( state === 3 || state === 4 ) {
if ( c === t.IAC || (cc >= 1 && cc <= 4) ) {
if ( st ) { st = false; out += '" '; }
if ( c === t.IAC ) {
out += 'IAC ';
state = 0; }
else if ( cc === 3 ) { out += 'MSDP_OPEN '; }
else if ( cc === 4 ) { out += 'MSDP_CLOSE '; }
else {
if ( state === 3 ) { out += 'MSSP_'; }
else { out += 'MSDP_'; }
if ( cc === 1 ) { out += 'VAR '; }
else { out += 'VAL '; }
}
} else {
if ( !st ) { st = true; out += '"'; }
out += c;
}
continue;
}
// NAWS Sequence
else if ( state === 5 ) {
if ( c === t.IAC ) {
st = false; out += 'IAC ';
state = 0;
} else {
if ( st === false ) { st = cc * 255; }
else {
out += (cc + st).toString() + ' ';
st = false;
}
}
continue;
}
// CHARSET Sequence
else if ( state === 6 ) {
if ( c === t.IAC || (cc > 0 && cc < 8) ) {
if ( st ) { st = false; out += '" '; }
if ( c === t.IAC ) {
out += 'IAC ';
state = 0; }
else if ( cc === 1 ) { out += 'REQUEST '; }
else if ( cc === 2 ) { out += 'ACCEPTED '; }
else if ( cc === 3 ) { out += 'REJECTED '; }
else if ( cc === 4 ) { out += 'TTABLE-IS '; }
else if ( cc === 5 ) { out += 'TTABLE-REJECTED '; }
else if ( cc === 6 ) { out += 'TTABLE-ACK '; }
else if ( cc === 7 ) { out += 'TTABLE-NAK '; }
} else {
if ( !st ) { st = true; out += '"'; }
out += c;
}
}
// ZMP Sequence
else if ( state === 7 ) {
if ( c === t.IAC || cc === 0 ) {
if ( st ) { st = false; out += '" '; }
if ( c === t.IAC ) {
out += 'IAC ';
state = 0; }
else if ( cc === 0 ) { out += 'NUL '; }
} else {
if ( !st ) { st = true; out += '"'; }
out += c;
}
}
// Normal Sequence
else if ( state < 2 ) {
out += i2w(c) + ' '; }
if ( state === 0 ) {
if ( c === t.SB ) { state = 1; }
} else if ( state === 1 ) {
if ( c === t.TTYPE || c === t.TSPEED ) { state = 2; }
else if ( c === t.MSSP ) { state = 3; }
else if ( c === t.MSDP ) { state = 4; }
else if ( c === t.NAWS ) { state = 5; }
else if ( c === t.CHARSET ) { state = 6; }
else if ( c === t.SENDLOC ) { state = 6; }
else if ( c === t.GMCP ) { state = 6; }
else if ( c === t.ZMP ) { state = 7; }
else { state = 0; }
}
}
return out.substr(0, out.length-1);
}
///////////////////////////////////////////////////////////////////////////////
// TELOPTS
///////////////////////////////////////////////////////////////////////////////
/** Handles the telopt TTYPE. */
var tTTYPE = function(decaf) { this.decaf = decaf; }
tTTYPE.prototype.current = -1
tTTYPE.prototype._dont = tTTYPE.prototype.disconnect = function() { this.current = -1; }
tTTYPE.prototype._sb = function(data) {
if ( data !== t.ECHO ) { return; }
this.current = (this.current + 1) % this.decaf.options.ttypes.length;
this.decaf.debugString('RCVD ' + DecafMUD.debugIAC(t.IAC + t.SB + t.TTYPE + t.ECHO + t.IAC + t.SE));
this.decaf.sendIAC(t.IAC + t.SB + t.TTYPE + t.IS + this.decaf.options.ttypes[this.current] + t.IAC + t.SE);
return false; // We print our own debug info.
}
DecafMUD.plugins.Telopt[t.TTYPE] = tTTYPE;
/** Handles the telopt ECHO. */
var tECHO = function(decaf) { this.decaf = decaf; }
tECHO.prototype._will = function() {
if ( this.decaf.ui ) { this.decaf.ui.localEcho(false); } }
tECHO.prototype._wont = tECHO.prototype.disconnect = function() {
if ( this.decaf.ui ) { this.decaf.ui.localEcho(true); } }
DecafMUD.plugins.Telopt[t.ECHO] = tECHO;
/** Handles the telopt NAWS. */
var tNAWS = function(decaf) { this.decaf = decaf; }
tNAWS.prototype.enabled = false;
tNAWS.prototype.last = undefined;
tNAWS.prototype._do = function() { this.last = undefined; this.enabled = true;
var n=this; setTimeout(function(){n.send();},0); }
tNAWS.prototype._dont = tNAWS.prototype.disconnect = function() { this.enabled = false; }
tNAWS.prototype.send = function() {
if ((!this.decaf.display) || (!this.enabled)) { return; }
var sz = this.decaf.display.getSize();
if ( this.last !== undefined && this.last[0] == sz[0] && this.last[1] == sz[1] ) { return; }
this.last = sz;
var data = String.fromCharCode(Math.floor(sz[0] / 255));
data += String.fromCharCode(sz[0] % 255);
data += String.fromCharCode(Math.floor(sz[1] / 255));
data += String.fromCharCode(sz[1] % 255);
data = t.IAC + t.SB + t.NAWS + data.replace(/\xFF/g,'\xFF\xFF') + t.IAC + t.SE;
this.decaf.sendIAC(data);
}
DecafMUD.plugins.Telopt[t.NAWS] = tNAWS;
/** Handles the telopt CHARSET. */
var tCHARSET = function(decaf) { this.decaf = decaf; }
//tCHARSET.prototype.connect = function() { this.decaf.sendIAC(t.IAC + t.WILL + t.CHARSET); }
tCHARSET.prototype._dont = function() { return false; }
tCHARSET.prototype._will = function() { var c = this; setTimeout(function() {
var cs = [], done = [];
// Add the current encoding first if not ISO-8859-1
var e = this.decaf.options.encoding;
if ( e !== 'iso88591' && DecafMUD.plugins.Encoding[e] !== undefined && DecafMUD.plugins.Encoding[e].proper !== undefined ) {
cs.push(DecafMUD.plugins.Encoding[e].proper);
done.push(e);
}
// Add the encodings in the order we want.
for(var i=0;i 0 ) {
// Attempt to read a control character.
var c = data.charCodeAt(0);
if ( c === 1 ) {
// MSDP_VAR. Read a variable name from data and reset the variable
// in out.
var ind = data.substr(1).search(msdp);
if ( ind === -1 ) {
variable = data.substr(1);
data = '';
} else {
variable = data.substr(1, ind);
data = data.substr(ind+1);
}
// Reset the variable, and continue.
out[variable] = undefined;
continue;
}
else if ( c === 4 ) {
// MSDP_CLOSE. Return what we have.
data = data.substr(1);
break;
}
// Make sure we have a variable name. If not, quit.
if ( variable === undefined ) {
return [out, ''];
}
if ( c === 2 ) {
// MSDP_VAL. Read a value. If variable isn't undefined, turn it into
// an array if it isn't one.
// Is this a MSDP_OPEN?
if ( data.charCodeAt(1) === 3 ) {
var o = readMSDP(data.substr(2));
val = o[0];
data = o[1];
} else {
var ind = data.substr(1).search(msdp), val = '';
if ( ind === -1 ) {
val = data.substr(1);
data = '';
} else {
val = data.substr(1, ind);
data = data.substr(ind+1);
}
}
// Check the existing variable.
if ( out[variable] === undefined ) {
out[variable] = val;
} else if ( typeof out[variable] === 'object' && out[variable].push !== undefined ) {
out[variable].push(val);
} else {
out[variable] = [out[variable], val];
}
continue;
}
// Still here? No command. Break then.
break;
}
return [out, data];
};
/** Convert a variable to a string of valid MSDP-formatted data.
* @param {any} obj The variable to convert. */
var writeMSDP = function(obj) {
var t = typeof obj;
if ( t === 'string' || t === 'number' ) { return obj.toString(); }
else if ( t === 'boolean' ) { return obj ? '1' : '0'; }
else if ( t === 'undefined' ) { return ''; }
// Must be an object.
else if ( t === 'object' ) {
var out = '';
for(var k in obj) {
// Don't write out undefineds and nulls.
if ( obj[k] === undefined || obj[k] === null || typeof obj[k] === 'function' ) { continue; }
out += '\x01' + k;
if ( typeof obj[k] === 'object' ) {
if ( obj[k].push !== undefined ) {
// Handle arrays differently than normal objects.
var v = obj[k], l = obj[k].length;
for(var i=0;i 0 && arguments[0] instanceof DecafMUD ) {
decaf = arguments[0];
off = 1;
} else {
// Since an instance wasn't specified, assume the latest instance.
decaf = DecafMUD.instances[DecafMUD.instances.length - 1];
off = 0;
}
// Get the language from our DecafMUD instance, then try getting the
// translated string.
lang = decaf.options.language;
if ( lang === 'en' ) { s = this; }
else {
if (!( DecafMUD.plugins.Language[lang] && (s = DecafMUD.plugins.Language[lang][this]) )) {
if ( String.logNonTranslated ) {
var l = DecafMUD.plugins.Language[lang] && DecafMUD.plugins.Language[lang]['English'] !== undefined ?
DecafMUD.plugins.Language[lang]['English'] : '"' + lang + '"';
console.warn('DecafMUD[' + decaf.id + '] i18n: No ' + l + ' translation for: ' +
this.replace(/\n/g,'\\n'));
}
s = this;
}
}
// Do replacements to make this even more useful.
if ( arguments.length - off === 1 && typeof arguments[off] === 'object' ) {
var obj = arguments[off];
for ( var i in obj ) {
s = s.replace('{'+i+'}', obj[i]);
}
} else {
var obj = arguments;
s = s.replace(/{(\d+)}/g, function(m) {
var p = parseInt(m[1]) + off;
return p < obj.length ? obj[p] : '';
});
}
// Return the fancy, translated, replaced string.
return s;
}
}
/** Display a dialog with About information for DecafMUD. */
DecafMUD.prototype.about = function() {
var abt = ["DecafMUD v{0} \u00A9 2010 Stendec"];
abt.push("http://decafmud.kicks-ass.net/\n");
abt.push("DecafMUD is a web-based MUD client written in JavaScript, rather" +
" than a plugin like Flash or Java, making it load faster and react as" +
" you'd expect a website to.\n");
abt.push("It's easy to customize as well, using simple CSS and JavaScript," +
" and free to use and modify, so long as your MU* is free to play!");
// Show the about dialog with a simple alert.
alert(abt.join('\n').tr(this, DecafMUD.version.toString()));
}
///////////////////////////////////////////////////////////////////////////////
// Debugging
///////////////////////////////////////////////////////////////////////////////
/** Write a string to the debug console. The type can be one of: debug, info,
* error, or warn, and defaults to debug. This does nothing if the console
* doesn't exist.
* @param {String} text The text to write to the debug console.
* @param {String} [type="debug"] The type of message. One of: debug, info, error, warn
* @param {Object} [obj] An object with extra details for use in the provided text.
* @example
* var details = {name: "Fred", bone: "tibia"};
* decaf.debugString("{name} broke their {bone}!", 'info', details);
*/
DecafMUD.prototype.debugString = function(text, type, obj) {
// Return if we don't have the console or a debug pane.
if (! 'console' in window ) { return; }
// Set the type to debug by default
if ( type === undefined ) { type = 'debug'; }
// Prepare the string. It's almost certain it won't be translatable, but
// the variable replacement is nice.
if ( obj !== undefined ) { text = text.tr(this, obj); }
// Firebug / Console Logging
if (!( 'console' in window )) { return; }
var st = 'DecafMUD[%d]: %s';
switch(type) {
case 'info': console.info(st, this.id, text); return;
case 'warn': console.warn(st, this.id, text); return;
case 'error': console.error(st, this.id, text); return;
default:
if ( 'debug' in console ) {
console.debug(st, this.id, text);
return;
}
console.log(st, this.id, text);
}
}
/** Show an error to the user, either via the interface if it's loaded or,
* failing that, a call to alert().
* @param {String} text The error message to display.
* @example
* decaf.error("My pants are on fire!");
*/
DecafMUD.prototype.error = function(text) {
// Print to debug
this.debugString(text, 'error');
// If we have console grouping, log the options.
if ( 'console' in window && console.groupCollapsed !== undefined ) {
console.groupCollapsed('DecafMUD['+this.id+'] Instance State');
console.dir(this);
console.groupEnd();
}
// If we have a UI, try splashError.
if ( this.ui && this.ui.splashError(text) ) { return; }
// TODO: Check the Interface and stuff
alert("DecafMUD Error\n\n{0}".tr(this,text));
}
///////////////////////////////////////////////////////////////////////////////
// Module Loading
///////////////////////////////////////////////////////////////////////////////
/** Load a script from an external file, using the given path. If a path isn't
* provided, find the path to decafmud.js and use that.
* @param {string} filename The name of the script file to load.
* @param {string} [path] The path to load the script from.
* @example
* decaf.loadScript("my-plugin-stuff.js");
*/
DecafMUD.prototype.loadScript = function(filename, path) {
if ( path === undefined ) {
if ( this.options.jslocation !== undefined ) { path = this.options.jslocation; }
if ( path === undefined || typeof path === 'string' && path.length === 0 ) {
// Attempt to discover the path.
var obj = document.querySelector('script[src*="decafmud.js"]');
if ( obj === null ) {
obj = document.querySelector('script[src*="decafmud.min.js"]'); }
if ( obj !== null ) {
path = obj.src.substr(0,obj.src.lastIndexOf('/')+1); }
}
}
// Now that we have a path, create a script element to load our script
// and add it to the header so that it's loaded.
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = path + filename;
document.getElementsByTagName('head')[0].appendChild(script);
// Debug that we've loaded it.
this.debugString('Loading script: ' + filename); // + ' (' + script.src + ')');
}
/** Require a moddule to be loaded. Plugins can call this function to ensure
* that their dependencies are loaded. Be sure to use this along with waitLoad
* to ensure the modules are loaded before calling code that uses them.
* @param {String} module The module that has to be loaded.
* @param {function} [check] If specified, this function will be used to check
* that the module is loaded. Otherwise, it will be looked for in the
* DecafMUD plugin tree.
* @example
* decaf.require('decafmud.encoding.cp437');
* @example
* // External module
* decaf.require('my-module', function() {
* return 'SomeRequirement' in window;
* }); */
DecafMUD.prototype.require = function(module, check) {
// If we're loading language files, try it.
if ( this.options.load_language && this.options.language !== 'en' &&
module.indexOf('language') === -1 && module.indexOf('decafmud') !== -1 ) {
var parts = module.split('.');
parts.splice(1,0,"language",this.options.language);
this.require(parts.join('.'));
}
if ( check === undefined ) {
// Build a checker
if ( module.toLowerCase().indexOf('decafmud') === 0 ) {
var parts = module.split('.');
if ( parts.length < 2 ) { return; } // Already have DecafMUD, duh.
parts.shift();
parts[0] = parts[0][0].toUpperCase() + parts[0].substr(1);
// If it's a telopt, search DecafMUD.TN for it.
if ( parts[0] === 'Telopt' ) {
for(var k in DecafMUD.TN) {
if ( parts[1].toUpperCase() === k.toUpperCase() ) {
parts[1] = DecafMUD.TN[k];
break; }
}
}
check = function() {
if ( DecafMUD.plugins[parts[0]] !== undefined ) {
if ( parts.length > 1 ) {
return DecafMUD.plugins[parts[0]][parts[1]] !== undefined;
} else { return true; }
}
return false;
};
} else {
throw "Can't build checker for non-DecafMUD module!"
}
}
// Increment required.
this.required++;
// Call the checker. If we already have it, return now.
if ( check.call(this) ) { return; }
// Load the script.
/*var decaf = this;
setTimeout(function() {
decaf.loadScript(module+'.js');
},this.required*500);*/
this.loadScript(module+'.js');
// Finally, push to need for waitLoad to work.
this.need.push([module,check]);
}
/** Wait for all the currently required modules to load. Then, after everything
* has loaded, call the supplied function to continue execution. This function
* calls itself on a timer to work without having to block. Since blocking is
* evil.
* @param {function} next The function to call when everything has loaded.
* @param {function} [itemloaded] If provided, this function will be called
* each time a new item has been loaded. Useful for splash screens.
*/
DecafMUD.prototype.waitLoad = function(next, itemloaded, tr) {
clearTimeout(this.loadTimer);
if ( tr === undefined ) { tr = 0; }
else if ( tr > this.options.wait_tries ) {
if ( this.need[0][0].indexOf('language') === -1 ) {
this.error("Timed out attempting to load the module: {0}".tr(this, this.need[0][0]));
return;
} else {
if ( itemloaded !== undefined ) {
if ( this.need.length > 1 ) {
itemloaded.call(this,this.need[0][0], this.need[1][0]);
} else {
itemloaded.call(this,this.need[0][0]);
}
}
this.need.shift();
tr = 0;
}
}
while( this.need.length ) {
if ( typeof this.need[0] === 'string' ) {
this.need.shift();
} else {
if ( this.need[0][1].call(this) ) {
if ( itemloaded !== undefined ) {
if ( this.need.length > 1 ) {
itemloaded.call(this,this.need[0][0], this.need[1][0]);
} else {
itemloaded.call(this,this.need[0][0]);
}
}
this.need.shift();
tr = 0;
} else { break; }
}
}
// If this.need is empty, call next. If not, call it again in a bit.
if ( this.need.length === 0 ) {
next.call(this);
} else {
var decaf = this;
this.loadTimer = setTimeout(function(){decaf.waitLoad(next,itemloaded,tr+1)},this.options.wait_delay);
}
}
///////////////////////////////////////////////////////////////////////////////
// Initialization
///////////////////////////////////////////////////////////////////////////////
/** The first step of initialization after loading the user interface. Here, we
* create a new instance of the user interface and tell it to show a basic
* splash. Then, we start loading the other plugins.
*/
DecafMUD.prototype.initSplash = function() {
// Create the UI if we're using one. Which we always should be.
if ( this.options.interface !== undefined ) {
this.debugString('Attempting to initialize the interface plugin "{0}".'.tr(this,this.options.interface));
this.ui = new DecafMUD.plugins.Interface[this.options.interface](this);
this.ui.initSplash();
}
// Set the number of extra steps predicted after this step of loading for
// the sake of updating the progress bar.
this.extra = 3;
// Require plugins for: storage, socket, encoding, triggers, telopt
this.require('decafmud.storage.'+this.options.storage);
this.require('decafmud.socket.'+this.options.socket);
this.require('decafmud.encoding.'+this.options.encoding);
// Load them. This is the total number of required things thus far.
if ( this.ui && this.need.length > 0 ) { this.updateSplash(null,this.need[0][0],0); }
this.waitLoad(this.initSocket, this.updateSplash);
}
/** Update the splash screen as we load. */
DecafMUD.prototype.updateSplash = function(module,next_mod,perc) {
if ( ! this.ui ) { return; }
// Calculate the percentage.
if ( perc === undefined ) {
perc = Math.min(100,Math.floor(100*(((this.extra+this.required)-this.need.length)/(this.required+this.extra)))); }
if ( module === true ) {
// Don't do anything.
} else if ( next_mod !== undefined ) {
if ( next_mod.indexOf('decafmud') === 0 ) {
var parts = next_mod.split('.');
next_mod = 'Loading the {0} module "{1}"...'.tr(this, parts[1],parts[2]);
} else {
next_mod = 'Loading: {0}'.tr(this,next_mod);
}
} else if ( perc == 100 ) {
next_mod = "Loading complete.".tr(this);
}
this.ui.updateSplash(perc, next_mod);
}
/** The second step of initialization. */
DecafMUD.prototype.initSocket = function() {
this.extra = 1;
// Create the master storage object.
this.store = new DecafMUD.plugins.Storage[this.options.storage](this);
this.storage = this.store;
if ( this.ui ) {
// Push a junk element to need so the status bar shows properly.
this.need.push('.');
this.updateSplash(true,"Initializing the user interface...".tr(this));
// Set up the UI.
this.ui.load();
}
// Attempt to create the socket.
this.debugString('Creating a socket using the "{0}" plugin.'.tr(this,this.options.socket));
this.socket = new DecafMUD.plugins.Socket[this.options.socket](this);
this.socket.setup(0);
// Load the latest round.
this.waitLoad(this.initUI, this.updateSplash);
}
/** The third step. Now we're creating the UI. */
DecafMUD.prototype.initUI = function() {
// Finish setting up the UI.
if ( this.ui ) {
this.ui.setup(); }
// Now, require all our plugins.
for(var i=0; iGoogle Chrome or ' +
'Mozilla Firefox for ' +
'the best experience.';
this.ui.infoBar(msg.tr(this));
}
if ( (!this.options.autoconnect) || (!this.socket.ready)) { return; }
this.connect();
}
/** Attempt to connect to the server if we aren't. */
DecafMUD.prototype.connect = function() {
if ( this.connecting || this.connected ) { return; }
if ( this.socket_ready !== true ) { throw "The socket isn't ready yet."; }
this.connecting = true;
this.connect_try = 0;
this.debugString("Attempting to connect...","info");
// Show that we're connecting
if ( this.ui && this.ui.connecting ) {
this.ui.connecting(); }
// Set a timer so we can try again.
var decaf = this;
this.conn_timer = setTimeout(function(){decaf.connectFail();},this.options.connect_timeout);
this.socket.connect();
}
/** Called when the socket doesn't connect in a reasonable time. Resets the
* socket to try again. */
DecafMUD.prototype.connectFail = function() {
clearTimeout(this.conn_timer);
this.cconnect_try += 1;
// On the last one, just ride it out.
if ( this.connect_try > this.options.reconnect_tries ) { return; }
// Retry.
this.socket.close();
this.socket.connect();
// Set the timer.
var decaf = this;
this.conn_timer = setTimeout(function(){decaf.connectFail();},this.options.connect_timeout);
}
///////////////////////////////////////////////////////////////////////////////
// Socket Events
///////////////////////////////////////////////////////////////////////////////
/** Called by the socket when the socket is ready. Make note that the socket is
* available, and if desired start trying to connect. */
DecafMUD.prototype.socketReady = function() {
this.debugString("The socket is ready.");
this.socket_ready = true;
// If we've loaded, and autoconnect is on, try connecting.
if ( this.loaded && this.options.autoconnect ) {
this.connect();
}
}
/** Called by the socket when the socket connects. */
DecafMUD.prototype.socketConnected = function() {
this.connecting = false; this.connected = true; this.connect_try = 0;
clearTimeout(this.conn_timer);
// Get the host and stuff.
var host = this.socket.host, port = this.socket.port;
this.debugString("The socket has connected successfully to {0}:{1}.".tr(this,host,port),"info");
// Call telopt connected code.
for(var k in this.telopt) {
if ( this.telopt[k] && this.telopt[k].connect ) {
this.telopt[k].connect(); }
}
// Show that we're connected.
if ( this.ui && this.ui.connected ) {
this.ui.connected(); }
//if ( this.display ) {
// this.display.message('Connected.'.tr(this,host,port),'decafmud socket status'); }
}
/** Called by the socket when the socket disconnects. */
DecafMUD.prototype.socketClosed = function() {
clearTimeout(this.conn_timer);
this.connecting = false; this.connected = false;
this.debugString("The socket has disconnected.","info");
// Call telopt disconnected code.
for(var k in this.telopt) {
if ( this.telopt[k] && this.telopt[k].disconnect ) {
this.telopt[k].disconnect(); }
}
// Clear the buffer to ensure we don't enter into a bad state on reconnect.
this.inbuf = [];
// Should we be reconnecting?
if ( this.options.autoreconnect ) {
this.connect_try++;
if ( this.connect_try < this.options.reconnect_tries ) {
// Show the message, along with a 'reconnecting...' bit if possible.
if ( this.ui && this.ui.disconnected ) {
this.ui.disconnected(true); }
var d = this;
// Show a reconnect infobar
var s = this.options.reconnect_delay / 1000;
if ( this.ui && this.ui.immediateInfoBar && s >= 0.25 ) {
this.ui.immediateInfoBar("You have been disconnected. Reconnecting in {0} second{1}...".tr(this, s, (s === 1 ? '' : 's')),
'reconnecting',
s,
undefined,
[['Reconnect Now'.tr(this),function(){ clearTimeout(d.timer); d.socket.connect(); }]],
undefined,
function(){ clearTimeout(d.timer); }
); }
this.timer = setTimeout(function(){
d.debugString('Attempting to connect...','info');
if ( d.ui && d.ui.connecting ) {
d.ui.connecting(); }
d.socket.connect();
}, this.options.reconnect_delay);
return;
}
}
// Show that we disconnected.
if ( this.ui && this.ui.disconnected ) {
this.ui.disconnected(false); }
}
/** Called by the socket when data arrives. */
DecafMUD.prototype.socketData = function(data) {
// Push the text onto the inbuf.
this.inbuf.push(data);
// If we've finished loading, handle it.
if ( this.loaded ) {
this.processBuffer();
}
}
/** Called by the socket when there's an error. */
DecafMUD.prototype.socketError = function(data,data2) {
this.debugString('Socket Err: {0} d2="{1}"'.tr(this,data,data2),'error');
}
///////////////////////////////////////////////////////////////////////////////
// Data Processing
///////////////////////////////////////////////////////////////////////////////
/** Get an internal incoder from a formatted name. */
DecafMUD.prototype.getEnc = function(enc) {
enc = enc.replace(/-/g,'').toLowerCase();
return enc;
}
/** Change the active encoding scheme to the provided scheme.
* @param {String} enc The encoding scheme to use. */
DecafMUD.prototype.setEncoding = function(enc) {
enc = this.getEnc(enc);
if ( DecafMUD.plugins.Encoding[enc] === undefined ) {
throw '"'+enc+"' isn't a valid encoding scheme, or it isn't loaded."; }
this.debugString("Switching to character encoding: " + enc);
this.options.encoding = enc;
// Now, reroute functions for speed.
this.decode = DecafMUD.plugins.Encoding[enc].decode;
this.encode = DecafMUD.plugins.Encoding[enc].encode;
}
var iac_reg = /\xFF/g;
/** Send input to the MUD, as if typed by a player. This means it also goes out
* to the display and stuff. Escape any IAC bytes.
* @param {String} input The input to send to the server. */
DecafMUD.prototype.sendInput = function(input) {
if ( ! this.socket ) { throw "We don't have a socket yet. Just wait a bit!"; }
this.socket.write(this.encode(input + '\r\n').replace(iac_reg, '\xFF\xFF'));
if ( this.ui ) {
this.ui.displayInput(input); }
}
/** This function is a mere helper for decoding. It'll be overwritten. */
DecafMUD.prototype.decode = function(data) {
return DecafMUD.plugins.Encoding[this.options.encoding].decode(data); }
/** This function is a mere helper for encoding. It'll be overwritten. */
DecafMUD.prototype.encode = function(data) {
return DecafMUD.plugins.Encoding[this.options.encoding].encode(data); }
/** Read through data, only stopping for TELNET sequences. Pass data through to
* the display handler. */
DecafMUD.prototype.processBuffer = function() {
if ( ! this.display ) { return; }
var data = this.inbuf.join(''), IAC = DecafMUD.TN.IAC, left='';
this.inbuf = [];
// Loop through the string.
while ( data.length > 0 ) {
var ind = data.indexOf(IAC);
if ( ind === -1 ) {
var enc = this.decode(data);
this.display.handleData(enc[0]);
this.inbuf.splice(1,0,enc[1]);
break;
}
else if ( ind > 0 ) {
var enc = this.decode(data.substr(0,ind));
this.display.handleData(enc[0]);
left = enc[1];
data = data.substr(ind);
}
var out = this.readIAC(data);
if ( out === false ) {
// Ensure old data goes to the very beginning.
this.inbuf.splice(1,0,left + data);
break;
}
data = left + out;
}
}
/** Read an IAC sequence from the supplied data. Then return either the remaining
* data, or if a full sequence can't be read, return false.
* @param {String} data The data to read a sequence from.
* @returns {String|boolean} False if we can't read a sequence, else the
* remaining data. */
DecafMUD.prototype.readIAC = function(data) {
if ( data.length < 2 ) { return false; }
// If the second character is IAC, push an IAC to the display and return.
else if ( data.charCodeAt(1) === 255 ) {
this.display.handleData('\xFF');
return data.substr(2);
}
// If the second character is a GA or NOP, ignore it.
else if ( data.charCodeAt(1) === 249 || data.charCodeAt(1) === 241 ) {
return data.substr(2);
}
// If the second character is one of WILL,WONT,DO,DONT, read it, debug,
// and handle it.
else if ( "\xFB\xFC\xFD\xFE".indexOf(data.charAt(1)) !== -1 ) {
if ( data.length < 3 ) { return false; }
var seq = data.substr(0,3);
this.debugString('RCVD ' + DecafMUD.debugIAC(seq));
this.handleIACSimple(seq);
return data.substr(3);
}
// If it's an IAC SB, read as much as we can to get it all.
else if ( data.charAt(1) === t.SB ) {
//this.debugString('RCVD ' + DecafMUD.debugIAC(data.substr(0,10)));
var seq = '', l = t.IAC + t.SE;
var code = data.charAt(2);
data = data.substr(3);
if ( data.length === 0 ) { return false; }
while(data.length > 0) {
var ind = data.indexOf(l);
if ( ind === -1 ) { return false; }
if ( ind > 0 && data.charAt(ind-1) === t.IAC ) {
// Escaped. Continue
seq += data.substr(0,ind+1);
data = data.substr(ind+1);
continue;
}
seq += data.substr(0,ind);
data = data.substr(ind+1);
break;
}
var dbg = true;
if ( this.telopt[code] !== undefined && this.telopt[code]._sb !== undefined ) {
if ( this.telopt[code]._sb(seq) === false ) { dbg = false; }
}
if ( dbg ) {
if ( code === t.MSSP && console.groupCollapsed !== undefined ) {
console.groupCollapsed('DecafMUD['+this.id+']: RCVD IAC SB MSSP ... IAC SE');
console.dir(readMSDP(seq)[0]);
console.groupEnd('DecafMUD['+this.id+']: RCVD IAC SB MSSP ... IAC SE');
} else {
this.debugString('RCVD ' + DecafMUD.debugIAC(t.IAC + t.SB + code + seq + t.IAC + t.SE));
}
}
}
// Just push the IAC off the stack since it's obviously bad.
return data.substr(1);
}
/** Send a telnet sequence, writing it to debug as well.
* @param {String} seq The sequence to write out. */
DecafMUD.prototype.sendIAC = function(seq) {
this.debugString('SENT ' + DecafMUD.debugIAC(seq));
if ( this.socket ) { this.socket.write(seq); }
}
/** Handle a simple (DO/DONT/WILL/WONT) IAC sequence.
* @param {String} seq The sequence to handle. */
DecafMUD.prototype.handleIACSimple = function(seq) {
var t = DecafMUD.TN, o = this.telopt[seq.charAt(2)],
c = seq.charAt(2);
// Ensure we actually have this option to deal with.
if ( o === undefined ) {
if ( seq.charAt(1) === t.DO ) {
this.sendIAC(t.IAC + t.WONT + c); }
else if ( seq.charAt(1) === t.WILL ) {
this.sendIAC(t.IAC + t.DONT + c); }
return;
}
switch(seq.charAt(1)) {
case t.DO:
if (!( o._do && o._do() === false )) {
this.sendIAC(t.IAC + t.WILL + c); }
return;
case t.DONT:
if (!( o._dont && o._dont() === false )) {
this.sendIAC(t.IAC + t.WONT + c); }
return;
case t.WILL:
if (!( o._will && o._will() === false )) {
this.sendIAC(t.IAC + t.DO + c); }
return;
case t.WONT:
if (!( o._wont && o._wont() === false )) {
this.sendIAC(t.IAC + t.DONT + c); }
return;
}
}
///////////////////////////////////////////////////////////////////////////////
// Basic Permissions
///////////////////////////////////////////////////////////////////////////////
/** Request permission for a given option, as stored in the global settings
* object at the given path. This will ask the user if they want to allow
* an action or not, provided they haven't given an answer in the past.
*
* Since the user input may take some time, this will call the provided
* callback function with the result when the user makes a decision.
*
* @param {String} option The path to the option to check.
* @param {String} prompt The question to show to the user, asking them if it's
* alright to do whatever it is you're doing.
* @param {function} callback The function to call when we have an answer. */
DecafMUD.prototype.requestPermission = function(option, prompt, callback) {
var cur = this.store.get(option);
if ( cur !== undefined && cur !== null ) {
callback.call(this, !!(cur));
return; }
var decaf = this;
var closer = function(e) {
// Don't store a setting for next time, but return false for now.
callback.call(decaf, false);
},
help_allow = function() {
decaf.store.set(option, true);
callback.call(decaf, true);
},
help_deny = function() {
decaf.store.set(option, false);
callback.call(decaf, false);
};
// First, check for infobars in the UI. That's preferred.
if ( this.ui && this.ui.infoBar ) {
this.ui.infoBar(prompt, 'permission', 0, undefined,
[['Allow'.tr(this), help_allow], ['Deny'.tr(this), help_deny]], undefined, closer);
return; }
}
///////////////////////////////////////////////////////////////////////////////
// Default Settings
///////////////////////////////////////////////////////////////////////////////
DecafMUD.settings = {
// Absolute Basics
'startup': {
'_path': "/",
'_desc': "Control what happens when DecafMUD is opened.",
'autoconnect': {
'_type': 'boolean',
'_desc': 'Automatically connect to the server.'
},
'autoreconnect': {
'_type': 'boolean',
'_desc': 'Automatically reconnect when the connection is lost.'
}
},
'appearance': {
'_path': "display/",
'_desc': "Control the appearance of the client.",
'font': {
'_type': 'font',
'_desc': 'The font to display MUD output in.'
}
}
};
///////////////////////////////////////////////////////////////////////////////
// Default Options
///////////////////////////////////////////////////////////////////////////////
DecafMUD.options = {
// Connection Basics
host : undefined, // undefined = Website's Host
port : 4000,
autoconnect : true,
connectonsend : true,
autoreconnect : true,
connect_timeout : 5000,
reconnect_delay : 5000,
reconnect_tries : 3,
// Plugins to use
storage : 'standard',
display : 'standard',
encoding : 'iso88591',
socket : 'flash',
interface : 'simple',
language : 'autodetect',
// Loading Settings
jslocation : undefined, // undefined = This script's location
wait_delay : 25,
wait_tries : 1000,
load_language : true,
plugins : [],
// Storage Settings
set_storage : {
// There are no settings. Yet.
},
// Display Settings
set_display : {
handlecolor : true,
fgclass : 'c',
bgclass : 'b',
fntclass : 'fnt',
inputfg : '-7',
inputbg : '-0'
},
// Socket Settings
set_socket : {
// Flash Specific
policyport : undefined, // Undefined = 843
swf : 'http://drakkos.co.uk/decafmud/decaf/src/flash/DecafMUDFlashSocket.swf',
// WebSocket Specific
wsport : undefined, // Undefined = Flash policy port
wspath : '',
},
// Interface Settings
set_interface : {
// Elements
container : undefined,
// Fullscreen
start_full : false,
// Input Specific
mru : true,
mru_size : 15,
multiline : true,
clearonsend : false,
focusinput : true,
blurclass : 'mud-input-blur',
msg_connect : 'Press Enter to connect and type here...',
msg_connecting : 'DecafMUD is attempting to connect...',
msg_empty : 'Type commands here, or use the Up and Down arrows to browse your recently used commands.'
},
// Telnet Settings
ttypes : ['decafmud-'+DecafMUD.version,'decafmud','xterm','unknown'],
environ : {},
encoding_order : ['utf8'],
// Plugin Settings
plugin_order : []
};
// Expose DecafMUD to the outside world
window.DecafMUD = DecafMUD;
})(window);