Scripts/cagsdebug.js |
this.name = "cagsdebug";
this.author = "cag";
this.copyright = "2017 cag";
this.licence = "CC BY-NC-SA 3.0";
this.description = "debugging helper fns";
this.version = "1.0";
/* jshint elision:true, shadow:false, esnext: true, curly: false, maxerr: 1000, asi: true, laxbreak: true, undef: true, unused:true
*/
/* global log, worldScripts, Script, Vector3D, Quaternion
*/
(function(){
/* validthis: true
*/
"use strict";
// false prevents output, though functions are still tracked
// - except with start's header, if it's forced by a child fn's msg or by its stop's msg.
// So putting text in a stop will force output of start's header when there's no regular
// output from add, begin or end
// - useful for exceptional situations, where a fn quits early: put a stop w/ msg before the return
// and output start's header as well; only see the fn output when something interesting happens
this.$fns_watched = {
'shipSpawned': false,
'shipScoopedOther': false,
'update_one_Sighting': true,
'_update_some_Sighting': true,
'_update_Sightings': false,
'_delete_Sighting': true,
'_add_Sighting': true
};
// worldScripts.telescope.$cag.init();
this.$project_name = 'telescope';
this.$DEBUG_CONSOLE_WIDTH = 200;
this.$DEBUG_OFF = true;
//this.$DEBUG_OFF = false;
this._display_map = function _display_map( map ) {
var that = _display_map;
var _showProps = ( that._showProps = that._showProps || worldScripts.cagsdebug._showProps );
if( map.hasOwnProperty( 'map' ) ) {
map = map.map;
}
var tmp = _showProps( map, 'map', false, true, true );
const MFD_FRIENDLY = 1, // bounty === 0 && !markedForFines
MFD_UNSOCIABLE = 2, // bounty || markedForFines
MFD_ACTIVE = 4, // .target || defenseTargets.length > 0
MFD_HOSTILE = 8, // in_ents_Targets ||
MFD_NEARBY = 16, // distance < scannerRange
MFD_FARAWAY = 32, // distance > scannerRange
MFD_PROTECTED = 64; // .withinStationAegis
const MFD_SALVAGE = 1, // cargo, escape pods, derelicts
MFD_MINING = 2, // asteroids, boulders, splinters & metal fragments
MFD_WEAPONS = 4, // mines & missiles
MFD_TRADERS = 8, // ships .isTrader & escorts
MFD_POLICE = 16, // .isPolice
MFD_PIRATES = 32, // .isPirate & .isPirateVictim
MFD_MILITARY = 64, // scanClass === 'CLASS_MILITARY'
MFD_ALIENS = 128, // scanClass === 'CLASS_THARGOID'
MFD_STATION = 512, // scanClass === 'CLASS_STATION'
MFD_NAVIGATION = 512, // some stations & beacons (may include a ship if emitting a beacon)
MFD_CELESTIAL = 1024; // sun, planets, moons
tmp += '\ndynamic flags';
var flags = map.dynamicMFD;
if( flags ) {
if( flags & MFD_FRIENDLY ) tmp += ': FRIENDLY';
if( flags & MFD_UNSOCIABLE ) tmp += ': UNSOCIABLE';
if( flags & MFD_ACTIVE ) tmp += ': ACTIVE';
if( flags & MFD_HOSTILE ) tmp += ': HOSTILE';
if( flags & MFD_NEARBY ) tmp += ': NEARBY';
if( flags & MFD_FARAWAY ) tmp += ': FARAWAY';
if( flags & MFD_PROTECTED ) tmp += ': PROTECTED';
}
tmp += '\nstatic flags';
flags = map.staticMFD;
if( flags ) {
if( flags & MFD_SALVAGE ) tmp += ': SALVAGE';
if( flags & MFD_MINING ) tmp += ': MINING';
if( flags & MFD_WEAPONS ) tmp += ': WEAPONS';
if( flags & MFD_TRADERS ) tmp += ': TRADERS';
if( flags & MFD_POLICE ) tmp += ': POLICE';
if( flags & MFD_PIRATES ) tmp += ': PIRATES';
if( flags & MFD_MILITARY ) tmp += ': MILITARY';
if( flags & MFD_ALIENS ) tmp += ': ALIENS';
if( flags & MFD_STATION ) tmp += ': STATION';
if( flags & MFD_NAVIGATION ) tmp += ': NAVIGATION';
if( flags & MFD_CELESTIAL ) tmp += ': CELESTIAL';
}
log( tmp );
}
this._dump_map = function _dump_map() { // dump Sightings list for telescope
var that = _dump_map;
var cd = ( that.cd = that.cd || worldScripts.cagsdebug );
var ws = ( that.ws = that.ws || worldScripts.telescope );
var curr_S = ( that.curr_S = that.curr_S || ws.$curr_Sighting );
var pst = curr_S.ent || null;
var mapping = ws.$SightingsMap, maplen = mapping.length;
var map, ent, target, thargoids = '';
var out = '____ent_dist.rank index rel. dir.n person [entity description minus position, scanClass & status labels ...] [ve_colour, lb_size, ml_size]\n';
// var out = '____ent_dist.rank index rel. dir.n curr.grav.D person [entity description minus position, scanClass & status labels ...] [ve_colour, lb_size, ml_size]\n';
var i, padding = '________________';
var ent_dist, rank, index, rel_dir, gs_curr, entID, descrn, gs_maxed;
for( i = 0; i < maplen; i++ ) {
map = mapping[i]; ent = map.ent;
gs_curr = map.gs_curr_dist;
gs_maxed = gs_curr > 0 && gs_curr === map.gs_max_dist;
target = pst && pst === ent;
if(ent.isThargoid) { out += ent+'\n';
thargoids += '. dataKey = '+map.dataKey+', last_posn = '+map.last_posn+' roles = '+map.roles+'\n';
}
ent_dist = map.ent_dist;
if( ent_dist > 999999999 )
ent_dist = cd._number_str( ent_dist / 1000000000 ) + ' e9';
else
ent_dist = cd._number_str( ent_dist );
ent_dist = padding.slice( ent_dist.length-10 )+ ent_dist;
if( target ) {
// ent_dist = ent_dist.replace(/^_*/, '==>');
ent_dist = ent_dist[1] == '_' ? '=>' + ent_dist.substring(2) :
ent_dist[0] == '_' ? '|>' + ent_dist.substring(1) :
'>' + ent_dist;
}
rank = map.rank.toUpperCase();
index = i < 10 ? '__' + i : i > 99 ? i : '_' + i;
rel_dir = ws._relativeDirection( ent.position, map );
rel_dir = rel_dir ? padding.slice( rel_dir.length-9 ) + rel_dir : '???';
// gs_curr = cd._number_str( gs_curr );
// gs_curr = padding.slice( gs_curr.length-(gs_maxed ? 10 : 11) ) + gs_curr;
entID = ent.entityPersonality;
entID = entID === undefined ? 'xxxxx' : entID.toString();
entID = padding.slice( entID.length-6 ) + entID;
descrn = ent.toString();
descrn = descrn.replace(/position\: /, '' );
descrn = descrn.replace(/scanClass\: /, '__' );
descrn = descrn.replace(/CLASS_/, '' );
descrn = descrn.replace(/status\: /, '__' );
descrn = descrn.replace(/STATUS_/, '' );
out += ent_dist+'_'+rank
out += map.swapable ? '%' : '_';
out += ' ['+index+']'+(target ? '|>' : ' ')+'['+rel_dir+']'
+(target ? '|>' : ' ')
// +'['+gs_curr+(gs_maxed ? ' M' : ']')
+' #'+entID+' '
+descrn+' ['+map.ve_colour+', '+map.lb_size+', '+map.ml_size+']'+(target ? '<==' : '')
// +' Ms: '+ map.staticMFD.toString(2)+' Md: '+map.dynamicMFD.toString(2)
+' stat: '+ cd._number_str( map.staticMFD, 0, 2 ) + ' dyn: '+ cd._number_str( map.dynamicMFD, 0, 2)
+ (map.have_scanned ? ' h_s: ' + map.have_scanned :'')
+'\n';
}
out += 'ws.$SightingsMap.length = ' + mapping.length;
log(out);
if( thargoids ) log( 'Thargoids:\n' + thargoids );
}
this._rel2PS = function rel2PS( name, position, distance ) {
var cd = worldScripts.cagsdebug;
var ps = player && player.ship;
var ps_position = ps.position;
let posn = position.constructor === Vector3D ? position :
Array.isArray(position) ? new Vector3D( position ) :
position.position || null;
if( !posn ) {
return 'missing position for ' + name;
}
let dist = distance || posn.distanceTo( ps_position );
let padding = '________________';
let pname = padding.slice( name.length - 16 ) + name;
if( name === 'lightball' ) pname = '__' + pname;
let vector = posn.subtract( ps_position );
let fwd, right, up, rpt = '';
fwd = ( ps.vectorForward.angleTo( vector ) *180/3.1415927 ).toFixed();
right = ( ps.vectorRight.angleTo( vector ) *180/3.1415927 ).toFixed();
up = ( ps.vectorUp.angleTo( vector ) *180/3.1415927 ).toFixed();
rpt += pname + ' is ' + ( fwd <= 90 ? 'forward ' : 'astern ' ) + fwd + '°, '
+ ( right <= 90 ? ' starboard ' : ' port ') + right + '°, '
+ ( up <= 90 ? ' above ' : ' below ') + up
+ '° at ' + cd._number_str( dist ) + ' m';
// vector = vector.direction();
// fwd = ps.vectorForward.dot( vector ).toFixed(5);
// right = ps.vectorRight.dot( vector ).toFixed(5);
// up = ps.vectorUp.dot( vector ).toFixed(5);
// rpt += '\n\t' + pname + ' is ' + ( fwd >= 0 ? 'forward ' : 'astern ' ) + ( Math.acos(fwd) *180/3.1415927 ).toFixed() + '°, '
// + ( right >= 0 ? ' starboard ' : ' port ') + ( Math.acos(right) *180/3.1415927 ).toFixed() + '°, '
// + ( up >= 0 ? ' above ' : ' below ') + ( Math.acos(up) *180/3.1415927 ).toFixed()
// + '° at ' + cd._number_str( dist ) + ' m';
return rpt;
}
this._curr_S_report = function _curr_S_report() {
var cd = worldScripts.cagsdebug;
var cs = worldScripts.telescope.$curr_Sighting;
var ps = player && player.ship;
var scannerRange = ps.scannerRange;
let map = cs.map;
if( !map ) {
log('telescope', '_curr_S_report, $curr_Sighting.map is ' + map );
return;
}
let ent = cs.ent;
let map_ent_dist = cs.map.ent_dist;
let marker = cs.marker;
let lightball = cs.map.lightball;
let tmp = 'ps.speed = ' + ps.speed + ', viewDirection = ' + ps.viewDirection + ', for ' + ent;
tmp += '\n ' + cd._rel2PS( 'ent', ent, map.ent_dist );
if( marker )
tmp += '\n ' + cd._rel2PS( 'marker', marker );
if( lightball )
tmp += '\n ' + cd._rel2PS( 'lightball', lightball );
// let ent_posn = map.rank === 'ukn' ? map.last_posn : ent.position;
let marker_dist = scannerRange - 499.6;
if( map_ent_dist < scannerRange ) {
if( map_ent_dist < marker_dist )
marker_dist = map_ent_dist;
}
// let targ_dir = ent_posn.subtract( ps_position ).direction();
// let targ_posn = ps_position.add( targ_dir.multiply( marker_dist ) );//.add( speed_adj );
// tmp += '\n ' + cd._rel2PS( 'new marker', targ_posn, marker_dist );
log('telescope', tmp );
}
// debugger ///////////////////////////////////////////////////////////////////////////////////////
/* (function () {
WS.telescope.$cag = worldScripts.cagsdebug._debug_msgs();
WS.telescope.$cag.init();
})() // */
this._debug_msgs = function _debug_msgs() {
function break_line( str, limit ) { // break str into lines, avoiding splitting on an '=' if poss.
function trim( x ) { // return index of 1st non-whitespace char.
var space;
var index = x; // never modify arguments
do {
space = str[ index ];
if( space !== ' ' && space !== '\t' ) break;
index++;
} while( index < str.length );
return index;
}
function output( x ) { // push indices for next line; return true if finished w/ str
var index = x; // never modify arguments
strptr = trim( strptr );
lines.push( [ strptr, index ] );
index = trim( index );
if( index + limit >= str.length ) { // just tail end remains
lines.push( [ index, -1 ] );
return true; // finished w/ str
}
strptr = index; i = index + limit; first = -1; newline = -1;
return false; // still more to output
}
function _seek( dir ) { // walk str in direction dir across whitespace; return presence of an '='
var equals = false;
do { // scan in direction dir for end of whitespace or an '='
if( i + dir < 0 || i + dir >= str.length ) break; // protect limits
i += dir;
letter = str[ i ];
if( letter === '=' ) equals = true;
} while( letter === ' ' || letter === '\t' || letter === '=' );
i -= dir;
return equals; // return presence of an '='
}
var first = -1; var letter, i; var newline = -1;
var lines = []; var strptr = 0;
for( i = strptr + limit; i > strptr; i-- ) {
if( newline === -1 ) {
newline = str.indexOf( '\n', strptr );
if( newline >= strptr && newline <= i ) // respect imbedded newlines
if( output( newline + 1 ) ) break;
} else
newline = 0; // only check once per output()
letter = str[ i ];
if( letter === ' ' || letter === '\t' ) {
if( _seek( 1 ) || _seek( -1 ) ) { // look fwd & back for '='s; found a bad break, ie. has '='
if( first < 0 ) first = i; // save the first, just in case
} else {
if( output( i ) ) break; // i pts to 1st letter in break
continue;
}
}
if( i - strptr <= 20 ) { // no good break bound; reasonable lower limit on line length
if( first > -1 ) { // use previously found (bad) break
if( output( first ) ) break;
} else { // no break found, arbitrary cut using limit
if( output( strptr + limit ) ) break;
}
}
}
return lines;
}
function insert( str, frame ) { // insert str into frame's msg buffer
var msg_len = frame.msg.length;
var pad_len = frame.pad.length;
var str_len = str.length;
if( msg_len === 0 ) { // pad the 1st line
frame.len = pad_len; // will be added on output but need to account for its length now
}
if( str_len + frame.len > DEBUG_CONSOLE_WIDTH ) { // begin a new line
var lines = break_line( str, DEBUG_CONSOLE_WIDTH - pad_len );
var start, end, i;
for( i = 0; i < lines.length; i ++ ) {
[start, end] = lines[ i ];
frame.msg += str.substring( start, ( end === -1 ? str_len : end ) ) + '\n';
}
} else {
frame.msg += ( msg_len > 0 ? ' ' : '' ) + str;
}
var nl_index = frame.msg.lastIndexOf( '\n', msg_len );
if( nl_index > -1 ) { // calc length of last line in msg
frame.len = msg_len - nl_index; // '\n' has length of 1
} else {
frame.len = msg_len;
}
}
function is_watched_fn( dbgfn, str ) { // check user supplied $fns_watched to determine if msg should proceed
var caller = frame.caller;
if( fns_watched.length === 0 ) return true; // fn limiting not used
if( frame === null ) {
log( project_name, 'ERR: _debug, null frame: debug cmd outside start...stop sequence. is_watched_fn: '+dbgfn+': str = '+str+'\n############');
return true; // allow in hopes of locating errant cmd
}
if( !fns_watched.hasOwnProperty( caller ) ) return false; // fn limiting is in play but fn missing -> deny by default
if( fns_watched[ caller ] !== true ) // its in use, fn found but turned off
return false;
return true; // fn in list & set true!
}
function trim_space( str ) { // trim white space at start & end of line; -can't use .trim() as it removes line breaks!
return str.replace(/^[ \t\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]+|[ \t\u1680\u180e\u2000-\u200a\u202f\u205f\u3000\ufeff]+$/g, '');
}
function change_breaks( line ) { // collapse & convert all line breaks
return line.replace(/[\n\r\f\v\u00a0\u2028\u2029]+/g, '<BR>');
}
function output() { // called by stop to output or start to flush pre-existing msg
if( DEBUG_OFF ) return;
var incl_hd = frame.incl_hd;
var msg = frame.msg;
if( !incl_hd && !msg ) // unless directed to output hd, only log if there's a msg
return; // excl. header when no msg
if( incl_hd && frame.out_len > 0 ) // a force_hd not necess, as frame already has output
return;
var head = frame.head;
var out = curr_pad + ' ' + frame.caller + ':: '; // 1st line of output always starts w/ the caller (.len incr'd in .add)
if( head ) out += head; // .head contains a newline char
out += msg;
out = trim_space( out );
out = change_breaks( out );
out = out.replace( /(<BR>)+$/g, '' ); // trim any trailing line break
out = out.replace( /(<BR>)+/g, '\n' + curr_pad + ' ' ); // add padding to start of each line
if( out.slice( - curr_pad.length ) === curr_pad ) // out ends w/ just a pad
out = out.slice( 0, - curr_pad.length );
if( frame.logging ) log( project_name, out ); // writes to console and log file
else log( out ); // writes only to console
frame.out_len += out.length;
frame.head = ''; msg = ''; frame.len = 0;
frame.incl_hd = false; // reset for next time
}
function add_msg( via, str, format ) { // insert format str into msg buffer
if( !is_watched_fn( via, str ) ) return;
if( !str ) return;
if( stack.length === 0 )
log( project_name, 'ERR: debug, no debug frame: check for premature ".stop()" or add a ".start()" statement. .' + via + ': str = ' + str +'\n############');
insert( format, frame );
}
// add( str ) - append str to output msg
function add( str ) { // user fn to append str to msg
if( DEBUG_OFF ) return;
add_msg( 'add', str, str );
}
// begin( str ) - output str at start of a new line (prepends a newline)
function begin( str ) { // user fn to begin a new line w/ str
if( DEBUG_OFF ) return;
add_msg( 'begin', str, (frame.len > 0 ? '\n' + str : str) );
}
// end( str ) - output str at end of curr. line (appends a newline)
function end( str ) { // user fn to end the current line w/ str
if( DEBUG_OFF ) return;
add_msg( 'end', str, str +'\n' );
}
// log_file( str, [con_log], [caller] ) - stand-alone user fn to add output to log file
// - it ignores $DEBUG_OFF flag but terminates any further action if true
// - con_log flag can be used to output to console as well, in curr. frame
// - caller may be from any previous frame; if not found & con_log is true, output added to current frame
function log_file( str, con_log, caller ) { // output data to log file independent of the current frame
log( project_name, caller + ', ' + str ); // writes to log file
if( DEBUG_OFF ) return; // debugger if off, so just log msg
if( stack.length === 0 ) return; // no active frame
if( con_log === undefined ) return;
if( caller === undefined ) { // add str to curr. frame
add( str );
return;
}
var prev;
var len = stack.length;
for( var i = 0; i < len; i ++ ) {
prev = stack[ i ].caller;
if( prev === caller ) {
insert( str, prev ) // add str to caller's frame
return;
}
}
add( str ); // failed to locate caller, add to curr. frame
}
function Frame( c ) { this.caller = c; this.head = ''; this.msg = '';
this.pad = ''; this.out_len = 0; this.len = 0;
this.incl_hd = false; this.force_hd = 1; this.logging = false; }
var used_frames = [];
var used_frame_num = 0;
function free_frame( frame ) {
//log('telescope', 'free_frame, for caller = ' + frame.caller + ', pool totals ' + (used_frame_num + 1)+ ', on stack: ' + (stack.length - 1) );
frame.caller = null; frame.head = ''; frame.msg = '';
frame.pad = ''; frame.out_len = 0; frame.len = 0;
frame.incl_hd = false; frame.force_hd = 1; frame.logging = false;
used_frames.push( frame );
used_frame_num++;
}
function alloc_frame( caller ) {
var frame;
if( used_frame_num > 0 ) {
frame = used_frames.pop();
used_frame_num--
frame.caller = caller;
//log('telescope', 'alloc_frame for caller = ' + caller + ', allocating recycled frame, pool totals ' + used_frame_num + ', on stack: ' + (stack.length + 1) );
} else {
frame = new Frame( caller );
log('telescope', 'alloc_frame, for caller = ' + caller + ', allocating NEW frame, pool totals ' + used_frame_num + ', on stack: ' + (stack.length + 1) );
}
stack.push( frame );
return frame;
}
// start( caller, [hd], [force_hd], [logging] ) - open new debug level; caller is function's name (output toggled in $fns_watched)
// - hd is str (heading) that is output only if more str's comes in
// - force_hd, an int, over-rules this (default = 1; see Frame object).
// this give some context when a normally silent fn outputs a msg,
// by showing who called it. The higher the number, the farther up the frame stack
// headers are forced to output
// - logging is a boolean to also write to log file (vs. debug console only)
function start( caller, hd, // user fn to init a msg frame, start's hd string is not counted in len, so if
force_hd, logging ) { // no fn msg's between start & stop, no output
if( DEBUG_OFF ) return;
var len = stack.length;
if( len > 0 ) { // not the root frame
output(); // flush any pending output
var fn_name = caller !== undefined ? caller
: 'Anonomous' + stack.length;
var prev;
for( var i = len -1; i >= 0; i-- ) { // check not already on stack (fix for GDERM bug)
prev = stack[ i ].caller;
if( prev === fn_name ) {
free_frame( stack.splice( i, 1 )[0] );
curr_pad = curr_pad.slice( 0, -PAD_LEN ); // just remove that one entry, as we know we're not recursing
}
}
}
frame = alloc_frame( caller );
var tmp_pad;
if( stack.length % 2 === 0 ) tmp_pad = '========='; // alternate padding char's
else tmp_pad = '+++++++++';
frame.pad = tmp_pad.substr( 0, PAD_LEN );
curr_pad += frame.pad;
if( caller !== undefined ) {
frame.caller = caller;
frame.len += frame.caller.length;
} else {
log( project_name, 'ERR: _debug, invalid caller: function name needed for output to be formatted correctly. .start: str = ' + hd +'\n############');
}
if( frame === null ) {
log( project_name, 'ERR: _debug, internal error: unable to create frame; stack len = ' + len + ', .start: caller: '+caller+'(), str = ' + hd +'\n############');
}
if( logging !== undefined ) frame.logging = logging;
if( force_hd !== undefined ) frame.force_hd = force_hd;
if( hd !== undefined ) {
tmp_pad = curr_pad; // save current padding str and clear curr_pad
curr_pad = ''; // so we don't put any padding on the header
insert( hd + '\n', frame ); // a start header always ends its output line
curr_pad = tmp_pad; // restore current padding
}
frame.head = trim_space( frame.msg ); // move to head
frame.msg = '';
frame.len = 0; // reset len as header doesn't count
}
// _stop( caller, [str], [nolog] ) - close curr. level or regress to it if events interruped sequence; nolog suppresses log file output,
// over-ruling logging flag in start
function stop( caller, str, nolog ) { // user fn to terminate a frame, ouputting any pending messages
if( DEBUG_OFF ) return;
var have_str = str && str.length > 0; // debug
if( caller === undefined )
log( project_name, 'ERR: _debug, missing caller: function name needed for output to be formatted correctly. .stop: str = ' + str +'\n############');
if( frame === null )
log( project_name, 'ERR: _debug, no debug frame: add a "start()" statement. .stop: caller: '+caller+'(), str = ' + str +'\n############');
if( str && caller === frame.caller ) {
insert( '\n' + str, frame );
have_str = false;
}
if( nolog !== undefined ) frame.logging = !nolog; // nolog over-rules frame.logging
if( stack.length > 0 ) {
var msg_len = frame.msg.length;
output(); // flush any pending output
if( stack.length > 1 ) { // not the root frame
var force_hd = ( msg_len > 0 ? frame.force_hd : 0 );
if( force_hd > 0 ) { // set higher frame(s) to output
var i = stack.length;
while( i-- && force_hd > 0 ) {
stack[ i ].incl_hd = true;
force_hd--;
}
}
}
free_frame( frame );
frame = stack.pop();
if( str && caller === frame.caller ) { // for when a .stop is skipped
log( project_name, 'ERR: _debug, missed .stop(): ' + frame.caller + '() stopped while ' + caller + '() still active. .stop: str = ' + str +'\n############');
insert( '\n' + str, frame ); // still output msg
have_str = false;
}
curr_pad = curr_pad.slice( 0, -PAD_LEN );
} else {
frame = alloc_frame( caller ); // re-init safely after having lost track!
}
if( have_str ) log('telescope', 'debug.stop, failed to output: ' + str );
}
function reset() { // reset msg stack; meant for debug console but is also a user fn
var len;
len = stack.length;
while( len-- ) {
free_frame( stack[ len ] );
}
stack.length = 0; // this will clear all ref's to frame objects
frame = null;
PAD_LEN = 3;
curr_pad = '';
}
function init( project ) { // import local var's at runtime by calling this fn
var wc = worldScripts.cagsdebug;
if( project ) {
project_name = wc.$project_name = project;
} else {
project_name = wc.$project_name;
}
fns_watched = wc.$fns_watched;
DEBUG_CONSOLE_WIDTH = wc.$DEBUG_CONSOLE_WIDTH;
DEBUG_OFF = wc.$DEBUG_OFF;
}
var frame = null;
var stack = []; // stack built of Frame's when debugging fns are nested
var PAD_LEN = 3; // max 9 unless you enlarge 2 padding str's in start
var curr_pad = '';
var project_name, fns_watched, DEBUG_CONSOLE_WIDTH, DEBUG_OFF;
return { init: init,
add: add,
begin: begin,
end: end,
log_file: log_file,
reset: reset,
start: start,
stop: stop
};
}
// debugger ends //////////////////////////////////////////////////////////////////////////////////
this._reportError = function _reportError( err, func, parms, depth, goDeep ) {
// constants - adjust as needed
var FILE_LEN = 100; // cut-off len for file spec.
var FNAME_LEN = 40; // cut-off len for function name
var ARGS_LEN = 60; // cut-off len for arguments string
var STRING_LEN = 80; // cut-off for long strings
var IPAD = ' '; // inside padding, eg. after array open bracket, before close bracket
/*
err (required) value passed in the catch statement
func (required) function that caught the error
- can also be a string with the function's name if the
function is not named
eg. this.startUp = function() { ... -> pass 'startUp'
this.startUp = function startUp() { ... -> pass startUp
- using a named function has the advantage of reporting
any function properties
parms (optional) parameters passed to func
- if there are multiple parms, pass them in an array
- can also include function variables if you want more
information dumped
eg. this.shipSpawned = function shipSpawned( ship ) {
var dist = player.ship.position.distanceTo( ship ); ...
-> pass as parms: [ship, dist] to have both displayed
depth (optional) # levels to expose contents of parameters/properties
- default is 1, use 0 to suppress overly long output
eg. if a parm is an array of 3 items, 0 will print "<array of 3>",
1 will print "[ item1, item2, item3 ]"
- if item3 is an object: "[ item1, item2, <object of ..> ]"
2 will print the contents of each item, in this case,
expanding the object
3 will expand the object's properties, etc
(ditto for nested arrays)
- NB: depth > 2 will likely produce large output, eg. ships
- will accept a 2 item array if you want different depths for
parameters vs. properties, ie. [ parm depth, prop depth ]
eg. [ 2, 1 ]
goDeep (optional) boolean indicating whether or not to test hasOwnProperty
when exposing properties
- default is false meaning inherited properties won't be shown
- eg. if you dump player.ship, false shows 63 properties, while true
shows 226! (player properties + ship properties + entity properties)
- inherited properties are prefixed by a caret '^'
example usage:
=============
this.relativeDirection = function relativeDirection( position, map ) {
var that = relativeDirection;
var ws = ( that.ws = that.ws || worldScripts.telescope );
var ps = player && player.ship;
try {
var dist = map.ent.positon.distanceTo( ps ); <== position mis-spelled
...
return relative_dirn;
} catch( err ) {
log( 'telescope', ws._reportError( err, relativeDirection, [position, map], 1 ) );
if( debug )
throw err;
}
}
Latest.log output:
=================
18:48:27.293 [LogEvents]: Asteroid 28897 spawned at 413 km
18:48:27.898 [telescope]:
function relativeDirection() caught: TypeError: map.ent.positon is undefined
parameters: [ (70839.2, -96302.7, 664477), <object of 6> ]
properties: { ws: <object of 169> }
file: ../AddOns/Norby.cag.Telescope.oxp/Scripts/telescope.js
line: 6204, relativeDirection( [object Vector3D], [object Object] )
file: ../AddOns/Norby.cag.Telescope.oxp/Scripts/cagsdebug.js
line: 47, _display_map( [object Object] )
18:48:27.899 [script.javaScript.exception.unexpectedType]: ***** JavaScript exception (oolite-debug-console 1.89): TypeError: map.ent.positon is undefined
18:48:27.899 [script.javaScript.exception.unexpectedType]: ../AddOns/Basic-debug.oxp/Scripts/oolite-debug-console.js, line 1117.
*/
function trim_str( str ) {
var result, len = str.length;
if( len === 0 )
return '<empty string>';
result = str.replace( /[\u180e\u2000-\u200a\u202f\u205f\u3000]+/g, ' ' );
result = result.replace( /[\n]+/g, '\\n' ).replace( /[\t]+/g, '\\t' )
result = '"' + (len > STRING_LEN ? result.substr(0, STRING_LEN) + ' ...' : result) + '"';
return result
}
var padding = [];
function mkSpacePad( count ) {
if( typeof count === 'number' ) {
padding.length = count + 1;
return padding.join(' ');
}
return ' ';
}
function countObjKeys( obj, deep ) { // Object.keys( obj ).length only counts hasOwnProperty ones
var count = 0; // deep overrides goDeep
if( goDeep || deep ) {
for( let prop in obj )
if( prop ) // this is just to silence JSLint
count++;
} else {
count = Object.keys( obj ).length;
}
return count;
}
function rptType( obj ) {
if( Array.isArray( obj ) ) {
let len = obj.length;
return len > 0 ? '<array of ' + len + '>' : '[]';
} else if( obj instanceof Script ) {
return '[Script "' + obj.name + '" version ' + obj.version + ']';
} else if( typeof obj === 'object' ) {
let len = countObjKeys( obj, true ); // ignore goDeep when counting
return len > 0 ? '<object of ' + len + '>' : '{}';
} else {
return obj;
}
}
function hasComplex( obj ) {
for( let prop in obj ) {
if( goDeep || obj.hasOwnProperty( prop ) ) {
let item = obj[ prop ];
if( Array.isArray( item ) || (typeof item === 'object' && item !== null) )
return true;
}
}
return false;
}
function showComplex( obj, recurse ) {
var isArray = Array.isArray( obj );
var len = isArray ? obj.length : countObjKeys( obj );
if( len === 0 ) return isArray ? '[]' : '{}';
var index = 0,
str = (isArray ? '[' : '{') + IPAD,
strLen = str.length;
var recursable = recurse > 0 && hasComplex( obj );
for( let prop in obj ) {
if( goDeep || obj.hasOwnProperty( prop ) ) {
let item = obj[ prop ];
let propStr = isArray ? '' :
(goDeep && !obj.hasOwnProperty( prop ) ? '^' : '') + prop + ': ';
let propLen = propStr.length;
str += propStr;
if( recursable ) {
if( index === 0 ) {
outStarts.push( (outStarts.length > 0
? outStarts[outStarts.length-1] + propLen + strLen
: strLen + propLen + strLen) );
}
str += fmt_parm( item, recurse );
if( index < len - 1 ) { // not the last one
let inset = outStarts.length > 1 ? outStarts[outStarts.length-2] : strLen;
str += ',\n' + mkSpacePad( indentLen + inset );
} else {
str += IPAD;
}
} else {
str += hasComplex( item ) ? rptType( item ) : fmt_parm( item, 0 );
str += index < len - 1 ? ', ' : IPAD;
}
index++;
}
}
if( recursable && index ) outStarts.pop();
return str + (isArray ? ']' : '}');
}
var outStarts = []; // stack of running total of recursed insets
var parents = []; // check parm not in parents to avoid endless recursion
function fmt_parm( parm, recurse ) {
if( parents.indexOf( parm ) < 0 ) {
parents.push( parm );
} else {
return parm;
}
var type = typeof parm;
var str = '';
if( parm === null ) {
str += 'null';
} else if( type === 'undefined' ) {
str += 'undefined';
} else if( type === 'string' ) {
str += trim_str( parm );
} else if( type === 'boolean' ) {
str += (parm ? 'true' : 'false');
} else if( type === 'function' ) {
str += 'function ' + parm.name + '()';
} else if( parm instanceof Script ) {
str += '[Script "' + parm.name + '" version ' + parm.version + ']';
} else if( parm instanceof Vector3D ) {
str += 'Vector3D: (' + parm.x.toFixed() + ', '
+ parm.y.toFixed() + ', ' + parm.z.toFixed() + ')';
} else if( parm instanceof Quaternion ) {
str += 'Quaternion: (' + parm.w.toFixed() + ' + ' + parm.x.toFixed() + 'i + '
+ parm.y.toFixed() + 'j + ' + parm.z.toFixed() + 'k)';
} else if( Array.isArray( parm ) ) {
str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
} else if( type === 'object' && parm ) {
str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
} else {
str += rptType( parm );
}
parents.pop();
return str;
}
var funcProps = {};
function propsNotName( obj ) {
if( typeof obj !== 'function' ) return 0; // backwards compatibity
for( let key in funcProps ) { // reset object
if( funcProps.hasOwnProperty( key ) )
delete funcProps[ key ];
}
for( let key in obj ) {
if( key !== 'name' )
funcProps[ key ] = obj[ key ];
}
return Object.keys( funcProps ).length;
}
var parmsLabel = '\n parameters: ';
var indentLen = parmsLabel.length - 1; // -1 for \n
var fnName = typeof func === 'function' ? func.name : func; // backwards compatibity
var rpt, parmMax, propMax,
bonus = Array.isArray( parms ) ? 1 : 0; // don't count parms being an array as recursion (+ 1)
if( Array.isArray( depth ) ) {
parmMax = (depth.length > 0 && typeof depth[ 0 ] === 'number' ? ~~(depth[ 0 ]) : 1) + bonus;
propMax = (depth.length > 1 && typeof depth[ 1 ] === 'number' ? ~~(depth[ 1 ]) : 1) + bonus;
} else {
parmMax = propMax = (typeof depth === 'number' ? ~~(depth) : 1) + bonus;
}
if( err instanceof Error ) {
rpt = '\nfunction ' + fnName + '() \t caught: \t' + err.name + ': ' + err.message;
} else { // for thrown strings (user defined errors)
rpt = '\nfunction ' + fnName + '() \t caught: \t' + err;
}
if( parms ) {
rpt += parmsLabel + fmt_parm( parms, parmMax );
}
if( propsNotName( func ) ) {
parmsLabel = '\n properties: ';
indentLen = parmsLabel.length - 1; // -1 for \n
rpt += parmsLabel + fmt_parm( funcProps, propMax + 1 ); // + 1 as funcProps is an object
}
// err is the stack object with properties: message, fileName, lineNumber, stack, name
// - stack is a long string containing <function call>@<filename>:<line #> separated by
// '\n' for each call in the stack
if( err && err.stack ) {
var lastFile, parsed, frame, fnCall, args, file, line, pad;
var stk = err.stack.split( /[\n\r]+/ ); // split on line breaks
for( let i = 0, len = stk.length; i < len; i ++ ) {
// stack line format: fn(parms)@../AddOns/.../script.js:123
parsed = stk[ i ].match( /^\s*(\w+)\((.*?)\)@(.*?):(.*?)$/ );
if( !parsed || parsed.length < 5 ) break;
[frame, fnCall, args, file, line] = parsed;
if( file && file !== lastFile ) { // suppress repeat of same filename
if( file.length > FILE_LEN )
file = file.substring( file.length - FILE_LEN ) + '...';
rpt += '\n file: ' + file;
lastFile = file;
}
pad = line < 10 ? ' ' : line < 100 ? ' ' : line < 1000 ? ' ' : '' ;
rpt += '\n line: ' + pad + line + ', ';
if( fnCall.length > FNAME_LEN ) fnCall = fnCall.substring(0, FNAME_LEN) + '...';
if( args.length > ARGS_LEN ) args = args.substring(0, ARGS_LEN) + '...';
if( args.length ) // add spaces inside function's parenthices
args = ' ' + args.replace( /,/g, ', ' ) + ' ';
rpt += fnCall + '(' + args + ')';
}
}
return rpt;
}
this._number_str = function _number_str( n, fixed, base ) {
var that = _number_str;
var round = ( that.round = that.round || Math.round );
var working = ( that.working = that.working || [] );
var comma, index, wk, len, str;
str = typeof n === 'string' ? parseFloat( n ) : n;
if( !isFinite( str ) ) return n.toString();
str = fixed > 0 ? str.toFixed( fixed ).toString( base || 10 )
: round( str ).toString( base || 10 );
len = str.length;
working.length = wk = 0;
for( index = 0; index < len; index++ ) working[ index ] = str[ index ];
index = str.indexOf( '.' );
comma = index >=0 ? (index > 0 ? index - 1 : 0) : len - 1;
for( ; comma > 0 ; comma -= 3 ) {
index = comma - 2;
if( (n < 0 ? index-1 : index) <= 0 ) break;
for( wk = len - 1; wk >= index; wk-- )
working[ wk + 1 ] = working[ wk ];
working[ index ] = ',';
len = working.length;
}
return working.join( '' );
}
// worldScripts.cagsdebug
// worldScripts.cagsdebug._showProps( cs.map, 'map' )
// worldScripts.cagsdebug._showProps( cs.map, 'map', false, false, true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', true, false, true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', false, true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', false, true, true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', true, true, true )
// worldScripts.cagsdebug._showProps( cs.map, 'map', false, 2, true )
// worldScripts.cagsdebug._showProps =
this._showProps = function _showProps( obj, objName, newLine, show_deep, expand_arrays, show_type ) {
// default: true true (1) true false
function trim_str( str ) {
var result, len = str.length;
if( len === 0 )
return '<empty string>';
result = str.replace( /[\u180e\u2000-\u200a\u202f\u205f\u3000]+/g, ' ' );
result = result.replace( /[ ]{3:}/g, ' ' );
result = result.replace( /[\n]+/g, '\\n' ).replace( /[\t]+/g, '\\t' )
// result = str.replace( /[\s]+/g, ' . ' );
result = '"' + (len > 90 ? result.substr(0, 90) + ' ...' : result) + '"';
return result
}
function mkPad( i, suppress ) {
if( i <= 0 || suppress ) return '';
padding.length = newLine ? i : 1;
return (newLine ? '.' :'') + padding.join( ' ' ); // '.' needed for console, as leading space trimmed
}
function rptType( obj, showIt ) {
if( Array.isArray( obj ) )
return show_type || showIt ? ' <array: ' + obj.length + ' elements> ' : '';
else if( typeof obj === 'object' )
return show_type || showIt ? ' <object: ' + Object.keys( obj ).length + ' keys> ' : '';
else
return ' <' + typeof obj + '> ';
}
function show_array( array, recurse ) {
if( array.length === 0 ) return '[ ]';
ilevel++;
var str = '[ ';
var pad = mkPad( ilevel, !expand_arrays );
var len = array.length;
for( var index = 0; index < len; index ++ ) {
str += !expand_arrays || !newLine ? (index > 0 ? ', ' : '') : '\n';
str += pad;
if( expand_arrays )
str += index + ': ' + fmt_prop( array[ index ], recurse );
else
str += fmt_prop( array[ index ], (recurse <= 1 ? 0 : recurse - 1) );
}
ilevel--;
if( len > 0 && expand_arrays && newLine ) str += '\n';
str += mkPad( ilevel, !len || !expand_arrays ) + ' ]' + rptType( array );
return str;
}
function show_obj( obj, recurse ) {
ilevel++;
var str = '{ ';
var pad = mkPad( ilevel );
var len = Object.keys( obj ).length;
for( var item in obj ) {
str += newLine ? '\n' : ' ';
str += pad;
if( Array.isArray( obj ) ) {
str += show_array( obj[ item ], recurse );
} else {
str += (obj.hasOwnProperty( item ) ? '' : '^') + item + ': ';
str += fmt_prop( obj[ item ], recurse );
}
str += ';';
}
ilevel--;
if( len > 0 && newLine ) str += '\n';
str += mkPad( ilevel, !len ) + ' }' + rptType( obj );
return str;
}
function fmt_prop( prop, recurse ) {
if( parents.indexOf( prop ) < 0 )
parents.push( prop );
else
return prop;
var type = typeof prop;
var str = '';
if( prop === null ) {
str += 'null';
} else if( type === 'undefined' ) {
str += 'undefined';
} else if( type === 'string' ) {
str += trim_str( prop ) + (show_type ? ' <string: ' + prop.length + ' char.s>' : '');
} else if( type === 'number' ) {
str += cd._number_str( prop, prop % 1 === 0 ? 0 : expand_arrays ? 3 : 1 )
+ (show_type ? ' <number>' : '');
} else if( type === 'boolean' ) {
str += prop + (show_type ? ' <boolean>' : '');
} else if( type === 'function' ) {
str += ' function ' + prop.name + '()';
} else if( Array.isArray( prop ) ) {
if( expand_arrays ) {
str += show_array( prop, (recurse <= 1 ? 0 : recurse - 1) );
} else {
str += rptType( prop, true );
}
} else if( type === 'object' && prop ) {
if( recurse > 0 ) {
str += show_obj( prop, (recurse <= 1 ? 0 : recurse - 1) );
} else {
str += rptType( prop, true );
}
} else {
str += prop + (show_type ? ' <'+type+'>' : '');
}
parents.pop();
return str;
}
var that = _showProps;
var cd = ( that.cd = that.cd || worldScripts.cagsdebug );
var padding = [], parents = [];
if( show_type === undefined ) show_type = false;
if( expand_arrays === undefined ) expand_arrays = true;
if( show_deep === undefined ) show_deep = 1;
if( newLine === undefined ) newLine = true;
var rmax = !show_deep ? 0 : show_deep === true ? 1 : ~~(show_deep);
var ilevel = 1;
return (newLine ? '\n' : ' ') + objName + ': ' + fmt_prop( obj, rmax ) + (newLine ? '\n' : '');
}
}).call(this);
|
Scripts/station_options.js |
this.name = "station_options";
this.author = "cag";
this.copyright = "2018 cag";
this.licence = "CC BY-NC-SA 4.0";
this.description = "Station interface for setting Telescope options.";
this.version = "1.1";
/* jshint elision: true, shadow: true, esnext: true, curly: false, maxerr: 1000, asi: true,
laxbreak: true, undef: true, unused: true, evil: true, forin: true, eqnull: true,
noarg: true, eqeqeq: true, boss: true, loopfunc: true, strict: true, nonew: true, noempty: false
*/
/*jslint indent: 4, white: true, debug: true, continue: true, sub: true, css: false, todo: true,
on: false, fragment: false, vars: true, nomen: true, plusplus: true, bitwise: true,
regexp: true, newcap: true, unparam: true, sloppy: true, eqeq: true, stupid: true
*/
/* global log, player, worldScripts, Script, Vector3D, Quaternion
*/
(function(){
/* validthis: true */
"use strict";
this.$O_initStationOptions = function _initStationOptions(
hostOxp, // reference to oxp script
keyPrefix, // string
optionsAllowedCallback, // reference to script function
callPWSG, // boolean
notifyCallback, // reference to script function
suppressSummary, // boolean
missionKeys // object
) {
/*
hostOxp
(required) a reference to your oxp, ie. worldScripts.myOxp
keyPrefix
(required) a string that is the common prefix to all of the entries
in your missiontext.plist that are to be used here (usually some
form of your oxp's name)
- see the missiontext-template.plist file
optionsAllowedCallback
(optional) a callback function in your oxp that returns true/false
indicating if your options facility is to be allowed
You may wish to restrict access to main stations only, systems of
a certain tech level/government, or based on device health
- default is to always allow the interface
callPWSG
(optional) a boolean indicating if you want playerWillSaveGame called
- if true, it will called after all the option changes have been
assigned to your oxp's variables
(assuming playerWillSaveGame is where you update your missionVariables)
notifyCallback
(optional) a callback function to notify your oxp that some option
have been altered. Your oxp variables will already have been set;
this is for cases where some follow-up code may need to be run
- it returns 2 arrays of strings:
1st is names of variables changed
2nd is names of options pages containing these
- see the missiontext-template.plist file
suppressSummary
(optional) indicates if you want the summary page displayed
when the player exits the interface after having made any changes
- default is to show it
- true will shut it down completely
The summary page also reminds (nags) the player to save game if
autosave is turned off, so "summary" will only suppress the summary
but continue to nag and for completeness, "autosave" will show the
summary but suppress the reminder
missionKeys
(optional) an object containing keys referenced in missiontext.plist,
When expandMissionText is called, the 2nd parameter is an object used
in the expansion. Since that is done here instead, a way is provided
to get your keys through (for substitution when inside square brackets)
- in addition to all the keys in station_options' missiontext.plist,
the following are also available:
stn_optns_clockString current value of clock.clockStringForTime( clock.adjustedSeconds )
stn_optns_curr_system name of current system
// the rest are oxp dependent
stn_optns_page name of the option page (from your _optionPages)
stn_optns_page_num number of current page (eg. "Page [stn_optns_page_num] of 5")
stn_optns_next_page_num number of next page (eg. "Switch to page [stn_optns_next_page_num]")
stn_optns_page_count number of pages currently loaded
stn_optns_option name of the current option when in an option page, null otherwise
stn_optns_changes_count number of changes made in current session
stn_optns_changed a map of {option: value} for changes made in current session
*/
try {
var that = _initStationOptions;
var so = (that.so = that.so || worldScripts.station_options);
if( !so._setInterfaces ) { // 1st time through, create the closure
let closure = so._options_closure();
so._setInterfaces = closure._setInterfaces;
so._registerHostOxp = closure._registerHostOxp;
so._purgepools = closure._purgepools;
so._resetLocalVars = closure._resetLocalVars;
// longer prefix needed to avoid circular assignment
so._$_getReminder4Oxp = closure._getReminder4Oxp;
so._$_getAllowedCallback = closure._getAllowedCallback;
so._$_getCallPWSG = closure._getCallPWSG;
so._$_getNotifyCallback = closure._getNotifyCallback;
so._$_getSuppressSummary = closure._getSuppressSummary;
so._$_getMissionKeys = closure._getMissionKeys;
so._$_setAllowedCallback = closure._setAllowedCallback;
so._$_setCallPWSG = closure._setCallPWSG;
so._$_setNotifyCallback = closure._setNotifyCallback;
so._$_setSuppressSummary = closure._setSuppressSummary;
so._$_setMissionKeys = closure._setMissionKeys;
so._$_updateMissionKeys = closure._updateMissionKeys;
// delayed these worldScripts to avoid unnecessary calls at startup
so.playerBoughtNewShip = so.playerBoughtEquipment =
so.equipmentAdded = so.equipmentRemoved = so.equipmentRepaired =
so.shipDockedWithStation = so.startUpComplete = so._setStationInterfaceEntries;
}
var registered = so._registerHostOxp( hostOxp, keyPrefix, optionsAllowedCallback,
callPWSG, notifyCallback, suppressSummary, missionKeys );
so._setStationInterfaceEntries();
return registered;
} catch( err ) {
// err, functn, parms, depth, goDeep
log( so.name, so._reportError( err, _initStationOptions,
[hostOxp, keyPrefix, optionsAllowedCallback, callPWSG,
notifyCallback, suppressSummary, missionKeys ] ) );
}
}
///////////////////////////////////////////////////////////////////////////////
// post init functions ////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// use these funcitons if you wish to alter the values you sent when
// you called $O_initStationOptions() to register
// Especially useful are the get/set/update functions for the missionKeys,
// which can override any of the keys defined in your missiontext.plist file
// and even any of those in this oxp's missiontext.plist (in case you want
// to change, for example, the text/color of the buttons or any other text)
this.$O_setAllowedCallback = function _setAllowedCallback( keyPrefix, newFunc ) {
var that = _setAllowedCallback;
var so = (that.so = that.so || worldScripts.station_options);
so._$_setAllowedCallback( keyPrefix, newFunc );
}
this.$O_getAllowedCallback = function _getAllowedCallback( keyPrefix ) {
var that = _getAllowedCallback;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getAllowedCallback( keyPrefix );
}
this.$O_setCallPWSG = function _setCallPWSG( keyPrefix, newBool ) {
var that = _setCallPWSG;
var so = (that.so = that.so || worldScripts.station_options);
so._$_setCallPWSG( keyPrefix, newBool );
}
this.$O_getCallPWSG = function _getCallPWSG( keyPrefix ) {
var that = _getCallPWSG;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getCallPWSG( keyPrefix );
}
this.$O_setNotifyCallback = function _setNotifyCallback( keyPrefix, newFunc ) {
var that = _setNotifyCallback;
var so = (that.so = that.so || worldScripts.station_options);
so._$_setNotifyCallback( keyPrefix, newFunc );
}
this.$O_getNotifyCallback = function _getNotifyCallback( keyPrefix ) {
var that = _getNotifyCallback;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getNotifyCallback( keyPrefix );
}
this.$O_setSuppressSummary = function _setSuppressSummary( keyPrefix, newBool ) {
var that = _setSuppressSummary;
var so = (that.so = that.so || worldScripts.station_options);
so._$_setSuppressSummary( keyPrefix, newBool );
}
this.$O_getSuppressSummary = function _getSuppressSummary( keyPrefix ) {
var that = _getSuppressSummary;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getSuppressSummary( keyPrefix );
}
this.$O_setMissionKeys = function _setMissionKeys( keyPrefix, newKeys ) {
var that = _setMissionKeys;
var so = (that.so = that.so || worldScripts.station_options);
so._$_setMissionKeys( keyPrefix, newKeys );
}
this.$O_getMissionKeys = function _getMissionKeys( keyPrefix ) {
var that = _getMissionKeys;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getMissionKeys( keyPrefix );
}
this.$O_updateMissionKeys = function _updateMissionKeys( keyPrefix, newKeys ) {
var that = _updateMissionKeys;
var so = (that.so = that.so || worldScripts.station_options);
so._$_updateMissionKeys( keyPrefix, newKeys );
}
/*
this fetches an object containing the current state of the summary
report mechanism (if you didn't register with suppressSummary = true)
.reportSummary boolean
.remindAutosave boolean
.autosaveStopRemind int, counter of how many times user reminded about Autosave
.suppressSummary boolean
*/
this.$O_getReminder4Oxp = function _getReminder4Oxp( keyPrefix ) {
var that = _getReminder4Oxp;
var so = (that.so = that.so || worldScripts.station_options);
return so._$_getReminder4Oxp( keyPrefix );
}
///////////////////////////////////////////////////////////////////////////////
// world event handlers ///////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
this.startUp = function startUp() {
var that = startUp;
var so = (that.so = that.so || worldScripts.station_options);
so.$Remind2Savegame = JSON.parse( missionVariables.$StationOptionsRemind2Savegame );
}
this._setStationInterfaceEntries = function _setStationInterfaceEntries() {
var that = _setStationInterfaceEntries;
var so = (that.so = that.so || worldScripts.station_options);
var ps = player && player.ship;
if( ps && ps.dockedStation ) {
if( so._setInterfaces ) { // undefined if have not called _initStationOptions
so._setInterfaces();
so.$optionsStarted = false; // flag used for hidding hud
}
}
}
this.shipWillLaunchFromStation = function shipWillLaunchFromStation() {
var that = shipWillLaunchFromStation;
var so = (that.so = that.so || worldScripts.station_options);
var ps = player && player.ship;
ps.hudHidden = false;
so.$optionsStarted = false;
}
this.playerWillSaveGame = function playerWillSaveGame( /*message*/ ) {
var that = playerWillSaveGame;
var so = (that.so = that.so || worldScripts.station_options);
missionVariables.$StationOptionsRemind2Savegame = JSON.stringify( so.$Remind2Savegame );
}
// rather than contend w/ the vagaries of guiScreenChanged,
// we simply hide HUD on entry (initOptions) & restore it here
this.missionScreenEnded = function missionScreenEnded() {
var that = missionScreenEnded;
var so = (that.so = that.so || worldScripts.station_options);
var ps = player && player.ship;
if( so.$optionsStarted ) {
ps.hudHidden = so.$hudHidden; // state save upon options entry
so.$optionsStarted = false;
so._resetLocalVars();
so._purgepools();
}
}
this._reportError = function _reportError( err, functn, parms, depth, goDeep ) {
// constants - adjust as needed
var FILE_LEN = 100; // cut-off len for file spec.
var FNAME_LEN = 60; // cut-off len for function name
var ARGS_LEN = 80; // cut-off len for arguments string
var STRING_LEN = 120; // cut-off for long strings
var IPAD = ' '; // inside padding, eg. after array open bracket, before close bracket
function trim_str( str ) {
var result, len = str.length;
if( len === 0 )
return '<empty string>';
result = str.replace( /[\u180e\u2000-\u200a\u202f\u205f\u3000]+/g, ' ' );
result = result.replace( /[\n]+/g, '\\n' ).replace( /[\t]+/g, '\\t' )
result = '"' + (len > STRING_LEN ? result.slice(0, STRING_LEN) + ' ...' : result) + '"';
return result
}
var padding = [];
function mkSpacePad( count ) {
if( typeof count === 'number' ) {
padding.length = count + 1;
return padding.join(' ');
}
return ' ';
}
function countObjKeys( obj, deep ) { // Object.keys( obj ).length only counts hasOwnProperty ones
var count = 0; // deep overrides goDeep
if( goDeep || deep ) {
for( let prop in obj )
if( prop ) // this is just to silence JSLint
count++;
} else {
count = Object.keys( obj ).length;
}
return count;
}
function rptType( obj ) {
if( Array.isArray( obj ) ) {
let len = obj.length;
return len > 0 ? '<array of ' + len + '>' : '[]';
} else if( obj instanceof Script ) {
return '[Script "' + obj.name + '" version ' + obj.version + ']';
} else if( typeof obj === 'object' ) {
let len = countObjKeys( obj, true ); // ignore goDeep when counting
return len > 0 ? '<object of ' + len + '>' : '{}';
} else {
return obj;
}
}
function hasComplex( obj ) {
for( let prop in obj ) {
if( goDeep || obj.hasOwnProperty( prop ) ) {
let item = obj[ prop ];
if( Array.isArray( item ) || (typeof item === 'object' && item !== null) )
return true;
}
}
return false;
}
function showComplex( obj, recurse ) {
var isArray = Array.isArray( obj );
var len = isArray ? obj.length : countObjKeys( obj );
if( len === 0 ) return isArray ? '[]' : '{}';
var index = 0,
str = (isArray ? '[' : '{') + IPAD,
strLen = str.length;
var recursable = recurse > 0 && hasComplex( obj );
for( let prop in obj ) {
if( goDeep || obj.hasOwnProperty( prop ) ) {
let item = obj[ prop ];
let propStr = isArray ? '' :
(goDeep && !obj.hasOwnProperty( prop ) ? '^' : '') + prop + ': ';
let propLen = propStr.length;
str += propStr;
if( recursable ) {
if( index === 0 ) {
outStarts.push( (outStarts.length > 0
? outStarts[outStarts.length-1] + propLen + strLen
: strLen + propLen + strLen) );
}
str += fmt_parm( item, recurse );
if( index < len - 1 ) { // not the last one
let inset = outStarts.length > 1 ? outStarts[outStarts.length-2] : strLen;
str += ',\n' + mkSpacePad( indentLen + inset );
} else {
str += IPAD;
}
} else {
str += hasComplex( item ) ? rptType( item ) : fmt_parm( item, 0 );
str += index < len - 1 ? ', ' : IPAD;
}
index++;
}
}
if( recursable && index ) outStarts.pop();
return str + (isArray ? ']' : '}');
}
var outStarts = []; // stack of running total of recursed insets
var parents = []; // check parm not in parents to avoid endless recursion
function fmt_parm( parm, recurse ) {
if( parents.indexOf( parm ) < 0 ) {
parents.push( parm );
} else {
return parm;
}
var type = typeof parm;
var str = '';
if( parm === null ) {
str += 'null';
} else if( type === 'undefined' ) {
str += 'undefined';
} else if( type === 'string' ) {
str += trim_str( parm );
} else if( type === 'boolean' ) {
str += (parm ? 'true' : 'false');
} else if( type === 'function' ) {
str += 'function ' + parm.name + '()';
} else if( parm instanceof Script ) {
str += '[Script "' + parm.name + '" version ' + parm.version + ']';
} else if( parm instanceof Vector3D ) {
str += 'Vector3D: (' + parm.x.toFixed() + ', '
+ parm.y.toFixed() + ', ' + parm.z.toFixed() + ')';
} else if( parm instanceof Quaternion ) {
str += 'Quaternion: (' + parm.w.toFixed() + ' + ' + parm.x.toFixed() + 'idx + '
+ parm.y.toFixed() + 'j + ' + parm.z.toFixed() + 'k)';
} else if( Array.isArray( parm ) ) {
str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
} else if( type === 'object' && parm ) {
str += showComplex( parm, recurse <= 1 ? 0 : recurse - 1 );
} else {
str += rptType( parm );
}
parents.pop();
return str;
}
var funcProps = {};
function propsNotName( obj ) {
if( typeof obj !== 'function' ) return 0; // backwards compatibity
for( let key in funcProps ) { // reset object
if( funcProps.hasOwnProperty( key ) )
delete funcProps[ key ];
}
for( let key in obj ) {
if( key !== 'name' && obj.hasOwnProperty( key ) )
funcProps[ key ] = obj[ key ];
}
return Object.keys( funcProps ).length;
}
var parmsLabel = '\n parameters: ';
var indentLen = parmsLabel.length - 1; // -1 for \n
var fnName = typeof functn === 'function' ? functn.name : functn; // backwards compatibity
var rpt, parmMax, propMax,
bonus = Array.isArray( parms ) ? 1 : 0; // don't count parms being an array as recursion (+ 1)
if( Array.isArray( depth ) ) {
parmMax = (depth.length > 0 && typeof depth[ 0 ] === 'number' ? ~~(depth[ 0 ]) : 1) + bonus;
propMax = (depth.length > 1 && typeof depth[ 1 ] === 'number' ? ~~(depth[ 1 ]) : 1) + bonus;
} else {
parmMax = propMax = (typeof depth === 'number' ? ~~(depth) : 1) + bonus;
}
if( err instanceof Error ) {
rpt = '\nfunction ' + fnName + '() \t caught: \t' + err.name + ': ' + err.message;
} else { // for thrown strings (user defined errors)
rpt = '\nfunction ' + fnName + '() \t caught: \t' + err;
}
if( parms ) {
rpt += parmsLabel + fmt_parm( parms, parmMax );
}
if( propsNotName( functn ) ) {
parmsLabel = '\n properties: ';
indentLen = parmsLabel.length - 1; // -1 for \n
rpt += parmsLabel + fmt_parm( funcProps, propMax + 1 ); // + 1 as funcProps is an object
}
// err is the stack object with properties: message, fileName, lineNumber, stack, name
// - stack is a long string containing <function call>@<filename>:<line #> separated by
// '\n' for each call in the stack
if( err && err.stack ) {
var lastFile, parsed, frame, fnCall, args, file, line, pad;
var stk = err.stack.split( /[\n\r]+/ ); // split on line breaks
for( let idx = 0, len = stk.length; idx < len; idx ++ ) {
// stack line format: fn(parms)@../AddOns/.../script.js:123
parsed = stk[ idx ].match( /^\s*(\w+)\((.*?)\)@(.*?):(.*?)$/ );
if( !parsed || parsed.length < 5 ) break;
[frame, fnCall, args, file, line] = parsed;
if( file && file !== lastFile ) { // suppress repeat of same filename
if( file.length > FILE_LEN )
file = file.slice( file.length - FILE_LEN ) + '...';
rpt += '\n file: ' + file;
lastFile = file;
}
pad = line < 10 ? ' ' : line < 100 ? ' ' : line < 1000 ? ' ' : '' ;
rpt += '\n line: ' + pad + line + ', ';
if( fnCall.length > FNAME_LEN ) fnCall = fnCall.slice(0, FNAME_LEN) + '...';
if( args.length > ARGS_LEN ) args = args.slice(0, ARGS_LEN) + '...';
if( args.length ) // add spaces inside function's parenthices
args = ' ' + args.replace( /,/g, ', ' ) + ' ';
rpt += fnCall + '(' + args + ')';
}
}
return rpt;
}
///////////////////////////////////////////////////////////////////////////////
// station options closure ////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
this.$hudHidden = false;
this.$optionsStarted = false;
this.$Remind2Savegame = null;
/* (function() { // station options IIFE for debugging
player.credits += 10000; // to buy stuff
if( worldScripts.logevents ) {
delete worldScripts.logevents.guiScreenChanged;
// delete worldScripts.logevents.guiScreenWillChange;
}
worldScripts.telescope.$DebugMessages = false;
worldScripts.telescope._reload_config();
})() // */
/* (function() { // station options IIFE for reloading new closure code
console.clearConsole();
var ws = worldScripts.telescope;
var so = worldScripts.station_options;
delete so._setInterfaces; // so closure will be re-invoked
// so.$O_initStationOptions( ws, 'telescope_', ws._stnOptionsAllowed, true, ws._reloadFromStn ); // , true , 'summary' , 'autosave'
ws._startStationOptions();
// for testing multiple oxps
//var fs = worldScripts.fps_monitor;
//so.$O_initStationOptions( fs, 'fps_monitor_', null, false, fs._reload_options );
so._setInterfaces();
// player.ship.hudHidden = false;
so.$optionsStarted = false;
})() // */
// ^gui screen.*?[\w\s]+(?=[\n]^[^g])
// : ' + + '
///
this._options_closure = function _options_closure() {
var so = worldScripts.station_options;
var cd = worldScripts.cagsdebug; /// disable B4publish
/*
* constants
*/
var ARROW = ' -> ', // these are for summary pages
COMMA = ', ', COLON = ':';
var PUNCTUATION = ',./<>?;\':"[]{}-=_+!@#$%^&*';
// from the wiki:
// "There are 21 lines available for display of the message and choices combined.
// (In 1.77, this is extended to 27 lines if the player's HUD is hidden)"
// - we always hide the HUD
// var MAXSCREENLINES = oolite.compareVersion( '1.77' ) > 0 ? 21 : 27; // 21 for versions < 1.77
var MAXSCREENLINES = 27; // 21 for versions < 1.77 but we require min ver 1.79
var MAXSCREENWIDTH = 32.04; // in em's, max length for text, oolite is a little forgiving
var SO_PREFIX = '$O_'; // need a unique prefix for station_options
/*
* system functions
*/
var floor = Math.floor, pow = Math.pow, abs = Math.abs,
strFontLen = defaultFont.measureString,
SpaceLen = strFontLen( ' ' ),
gameSettings = oolite.gameSettings;
/*
* oxp dependent variables from _initStationOptions
*/
var hostOxp, keyPrefix, optionsAllowedCallback, callPWSG,
notifyCallback, suppressSummary;
/*
* oxp dependent variables read/derived from missiontext
*/
var hostVarPrefix, lowestValue, highestValue, maxPrecision,
maxVectorLen, allow_reset, formatting,
optionPages = {}, pageLabels = [], optionTabStops = [];
/*
* other oxp dependent variables
*/
var decodeExprn, decodeTerm, // oxp specific regular expressions (see initREs)
decodeAssign, decodeFnCall;// "
var registeredOxps = {}; // dictionary of oxp specific variables
var optionInfo = {}; // dictionary of options for an oxp
/*
* local variables globally available ('glocals')
*/
var changesMade = {}; // dictionary of pending variable changes
var currentChoice = {}; // objects for info common to functions
var decodedChoice = {}; // - properties include key, type, defawlt, min, max, numBits, selection, error, currently
var missionKeys; // oxp specific map of expansion keys, initialized with defaultMissionKeys
var expandKeys = {}; // map of internal expansion keys used for expandText
var validOptions = {}; // working map of available options
// regular expressions used in more than 1 function; the rest are w/ their function
var matchListOfStr = /[^']+(?=(?:'\s*,\s*')|(?:'\s*[,]?\s*$))/g;
var matchNumbers = /([+-]?\d+[.]?\d*|\.\d+)/g;
var ps, currentScreenIdx;
/*
* objects
*/
var defaultMissionKeys = {
stn_optns_clockString: '',
stn_optns_curr_system: '',
stn_optns_buttons: {},
// the rest are oxp dependent
stn_optns_page: '',
stn_optns_page_num: 0,
stn_optns_next_page_num: 0,
stn_optns_page_count: 0,
stn_optns_option: null,
stn_optns_changes_count: 0,
stn_optns_changed: {},
// : , : , : , : , : , :
};
///?replace all changesMade w/ missionKeys.stn_optns_changed
/// - only a single store of changes
/// - allows oxp to alter
var infoTemplate = {
option: null, // text of option name
optionStr: null, // option display text; same as .option if rename ('=') not used
optionLen: 0, // defaultFont measure of option name
page: null, // name of page option (currently) resides on
type: null, // one of text, toggle, number, decimal, vector, bitflags, choice
selection: null, // for types bitflags, choice
numBits: null, // for type bitflags
allBits: null, // for type bitflags
// these next 3 may not be specified
defawlt: null, // default value
min: null, // range for types number, decimal, bitflags, choice & vector
max: null, // range for types number, decimal, bitflags, choice & vector
isConditional: false, // does option have a _condition in plist
isSuppressed: false, // current state if is conditional on other options
condition: null, // text of any _condition (if no substitutions)
controls: null, // list of options that depend on this ones value
relyOnOptions: null, // list of options this one relies on
relyOnHostVars: null, // list of hostVars this one relies on
hasAssign: false, // does option have a _assign in plist
assign: null, // text of any _assign (if no substitutions)
hasExecute: false, // does option have a _execute in plist
execute: null, // text of any _assign (if no substitutions)
currently: null, // current value of the option
};
///////////////////////////////////////////////////////////////////////////////
// exposed functions //////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// : ' + + '
function _getReminder4Oxp( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getReminder4Oxp, prefix ) );
} else { // return a copy
return getObject( getRemindObj( prefix ) );
}
}
function _notRegisteredMsg( caller, prefix ) {
var msg = (typeof caller === 'function' ? caller.name : caller);
if( !prefix ) {
msg += ', missing or invalid prefix in function call: ' + prefix;
} else {
msg += ', the host oxp "' + prefix
+ '" must register before using this function';
}
return msg;
}
function _setAllowedCallback( prefix, newFunc ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _setAllowedCallback, prefix ) );
} else {
registeredOxps[ prefix ].optionsAllowedCallback = newFunc;
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
optionsAllowedCallback = newFunc;
}
}
}
function _getAllowedCallback( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getAllowedCallback, prefix ) );
} else {
return registeredOxps[ prefix ].optionsAllowedCallback;
}
}
function _setCallPWSG( prefix, newBool ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _setCallPWSG, prefix ) );
} else {
registeredOxps[ prefix ].callPWSG = newBool;
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
callPWSG = newBool;
}
}
}
function _getCallPWSG( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getCallPWSG, prefix ) );
} else {
return registeredOxps[ prefix ].callPWSG;
}
}
function _setNotifyCallback( prefix, newFunc ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _setNotifyCallback, prefix ) );
} else {
registeredOxps[ prefix ].notifyCallback = newFunc;
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
notifyCallback = newFunc;
}
}
}
function _getNotifyCallback( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getNotifyCallback, prefix ) );
} else {
return registeredOxps[ prefix ].notifyCallback;
}
}
function _setSuppressSummary( prefix, newBool ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _setSuppressSummary, prefix ) );
} else {
registeredOxps[ prefix ].suppressSummary = newBool;
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
suppressSummary = newBool;
}
}
}
function _getSuppressSummary( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getSuppressSummary, prefix ) );
} else {
return registeredOxps[ prefix ].suppressSummary;
}
}
// create a NEW set of missionKeys by combining newKeys
// with defaultMissionKeys
// - if you want to preserve existing key, use _updateMissionKeys
function _setMissionKeys( prefix, newKeys ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _setMissionKeys, prefix ) );
} else {
let reg = registeredOxps[ prefix ];
freeObject( reg.missionKeys, true ); // purge the old set but not objects referenced
let defKeys = getObject( defaultMissionKeys );// make a copy of defaultMissionKeys
updateMissionKeys( prefix, defKeys, newKeys );// updates defKeys with newKeys
reg.missionKeys = defKeys;
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
missionKeys = defKeys;
}
}
}
// returns a COPY of missionKeys
// any changes to it will have no effect until you send it back
// using _setMissionKeys or _updateMissionKeys
function _getMissionKeys( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _getMissionKeys, prefix ) );
} else { // return a copy
return getObject( registeredOxps[ prefix ].missionKeys );
}
}
// add/updates the current missionKeys with the values in newKeys
function _updateMissionKeys( prefix, newKeys ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, _notRegisteredMsg( _updateMissionKeys, prefix ) );
} else {
updateMissionKeys( prefix, registeredOxps[ prefix ].missionKeys, newKeys );
if( so.$optionsStarted && prefix === keyPrefix ) {
// set glocal if oxp's instance is running
missionKeys = registeredOxps[ prefix ].missionKeys;
}
}
}
function updateMissionKeys( prefix, obj, update ) { // also called by _registerHostOxp
var that = updateMissionKeys;
var defaults = (that.defaults = that.defaults || Object.keys( defaultMissionKeys ));
var oxpsMissionKeys = registeredOxps[ prefix ].missionKeys;
for( let key in update ) {
if( update.hasOwnProperty( key ) ) {
// update map preserving defaultMissionKeys
if( defaults.indexOf( key ) >= 0 && update[ key ] !== oxpsMissionKeys[ key ] ) {
log( so.name, 'updateMissionKeys, key "' + key
+ '" belongs to station_options and is read-only; that update will be ignored' );
} else {
obj[ key ] = update[ key ];
}
}
}
}
///////////////////////////////////////////////////////////////////////////////
// registration functions /////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
function _registerHostOxp( oxpRef, prefix, allowedFunc,
doPWSG, notifyFunc, noReport, oxpKeys ) {
function initMissionKeys( keys ) {
var mKeys = getObject( defaultMissionKeys );// make a copy of defaultMissionKeys
if( keys ) {
for( let prop in keys ) {
if( keys.hasOwnProperty( prop ) ) {
mKeys[ prop ] = keys[ prop ];
}
}
}
return mKeys;
}
registerFailures.length = 0;
_resetLocalVars();
try{
if( registeredOxps.hasOwnProperty( prefix ) ) { // already registered!
// only happens (?) when reloading entire host script into debugger, update map
log( so.name, '_registerHostOxp, WARNING: missiontext prefix already registered: ' + prefix );
registeredOxps[ prefix ].hostOxp = oxpRef;
registeredOxps[ prefix ].optionsAllowedCallback = allowedFunc || null;
registeredOxps[ prefix ].callPWSG = doPWSG === undefined ? false : doPWSG;
registeredOxps[ prefix ].notifyCallback = notifyFunc || null;
registeredOxps[ prefix ].suppressSummary = noReport === undefined ? false : noReport;
registeredOxps[ prefix ].missionKeys = initMissionKeys( oxpKeys );
return false;
}
var title, summary, category; // options required for interface
missionKeys = initMissionKeys( oxpKeys ); // needs to be set for expandText
keyPrefix = prefix;
title = expandText( prefix + 'interface_title' );
if( !title || title.length === 0 )
registerFailures.push( prefix + 'interface_title' );
summary = expandText( prefix + 'interface_summary' );
if( !summary || summary.length === 0 )
registerFailures.push( prefix + 'interface_summary' );
category = expandText( prefix + 'interface_category' );
if( !category || category.length === 0 )
registerFailures.push( prefix + 'interface_category' );
if( registerFailures.length > 0 ) {
log( so.name, '_registerHostOxp, ERROR registering "' + prefix
+ '", failed to expand: ' + registerFailures.join(', ') );
return false;
}
registeredOxps[ prefix ] = {
hostOxp: oxpRef,
optionsAllowedCallback: allowedFunc || null,
callPWSG: doPWSG === undefined ? false : doPWSG,
notifyCallback: notifyFunc || null,
suppressSummary: noReport === undefined ? false : noReport,
missionKeys: missionKeys,
interface_title: title,
interface_summary: summary,
interface_category: category,
callback: function _initOptions() { initOptions( prefix ) }
};
// calling to check for errors at registration, not first usage
updatePageData( prefix, true ); // true => reportOnly
let verdict = updateRegistry( prefix );
_purgepools();
return verdict;
} catch( err ) {
log( so.name, so._reportError( err, _registerHostOxp,
[ oxpRef, prefix, allowedFunc,
doPWSG, notifyFunc, noReport, oxpKeys ] ) );
throw err;
}
}
function _resetLocalVars() { // clear any data from previous user/session
// values from _initStationOptions
hostOxp = keyPrefix = optionsAllowedCallback = callPWSG = notifyCallback = suppressSummary = undefined;
// values from missiontext
hostVarPrefix = lowestValue = highestValue = maxPrecision = maxVectorLen = allow_reset = formatting = undefined;
// derived values
decodeExprn = decodeTerm = decodeAssign = decodeFnCall = undefined;
// working objects
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references) as sent to hostOxp
if( missionKeys && missionKeys.hasOwnProperty( 'stn_optns_changed' ) ) {
clearObject( missionKeys.stn_optns_changed );// clear previous changes
}// do not clear missionKeys object as stored in registeredOxps
clearObject( currentChoice );
clearObject( decodedChoice );
clearObject( expandKeys );
clearObject( validOptions );
clearObject( optionPages );
clearObject( optionInfo );
missionKeys = undefined;
// working arrays
allOptions.length = 0;
pageLabels.length = 0;
optionTabStops.length = 0;
// local variables
currentScreenIdx = undefined;
ps = player && player.ship;
}
function initOptions( prefix ) {
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, 'initOptions, missiontext prefix NOT registered: ' + prefix );
return;
}
if( guiScreen != "GUI_SCREEN_INTERFACES" ) {
return;
}
_resetLocalVars();
keyPrefix = prefix;
// missionKeys must be initialized before any calls to expandText()
missionKeys = registeredOxps[ prefix ].missionKeys;
// initialize session
so.$optionsStarted = true;
so.$hudHidden = ps.hudHidden; // save HUD state
ps.hudHidden = true;
// from oolite JS ref: hudAllowsBigGui
// "Whether the HUD allows a "big GUI" or not. Most useful for determining
// whether mission screens will have 21 or 27 lines.
// If hudHidden is true this will also be true, even if the HUD which is
// hidden would not normally allow a big GUI."
MAXSCREENLINES = ps.hudAllowsBigGui ? 27 : 21;
currentChoice.option = null;
scroller.reset();
displayOptions();
}
// : ' + + '
var allOptions = [];
function updatePageData( reportOnly ) { // reportOnly used at registration to not return on errors
hostOxp = registeredOxps[ keyPrefix ].hostOxp;
hostVarPrefix = expandText( keyPrefix + 'hostVarPrefix' );
if( !hostVarPrefix || hostVarPrefix.length === 0 ) {
registerFailures.push( keyPrefix + 'hostVarPrefix' );
} else {
initREs( hostVarPrefix );
}
var optionPagesStr = expandText( keyPrefix + 'optionPages' );
if( !optionPagesStr || optionPagesStr.length === 0 ) {
registerFailures.push( keyPrefix + 'optionPages' );
} else {
let pages = optionPagesStr.match( matchListOfStr );
if( pages === null ) {
log(so.name, 'updatePageData, error parsing "' + keyPrefix + '_optionPages" key '
+ ', expandText() returned "' + optionPagesStr + '"' );
if( !reportOnly )
log(so.name, '\tError is critical, aborting.');
if( registerFailures.length )
log(so.name, 'also failed to expand: ' + registerFailures.join(', ') );
if( !reportOnly ) {
return false;
}
}
// save page labels, as object property order is indeterminate
pageLabels.length = 0;
let pageCount = parsePages( pages );
if( pageCount === 0 ) {
log(so.name, 'updatePageData, for missiontext keyPrefix "' + keyPrefix
+ '" error parsing pages: "' + pages + '"' );
if( !reportOnly )
log(so.name, '\tError is critical, aborting.');
if( registerFailures.length )
log(so.name, 'also failed to expand: ' + registerFailures.join(', ') );
if( !reportOnly ) {
return false;
}
}
// populate optionInfo even on registration (!reportOnly) for immediate
// error detection ie. don't have to F4 and open page(s) to find any errors
createOptionInfo( pages, allOptions );
}
return true;
}
// : ' + + '
function initREs( prefix ) { // oxp specific re's
// no lookbehinds in js
var reQuoted = "(?:'([^'\\\\]*(?:\\\\'[^'\\\\]*)*)')";
// - allows escaped single quotes
// - uses Friedl's: "unrolling-the-loop" technique
// ie. normal* (special normal*)* where special is an escaped single quote
var reDecimal = '([+-]?\\d*[.]\\d+|[+-]?\\d+[.]\\d*)';
var reInteger = '([+-]?\\d+)';
var reVector = '(\\[[^[]*\\])';
var reOptionOrHost = '[!\\s*]{0,}[' + prefix + ']?\\w+';
var reOperators = '([=!<>]+)';
var exprnTerm = '(' + reQuoted + '|' + reVector + '|' + reDecimal + '|' + reInteger
+ '|' + reOptionOrHost + ')';
decodeTerm = new RegExp( '(\\s*)' + exprnTerm, 'g' ); // see createOptionInfo
// (\s*)((?:'([^'\\]*(?:\\'[^'\\]*)*)')|(\[[^[]*\])|([+-]?\d*[.]\d+|[+-]?\d+[.]\d*)|([+-]?\d+)|[!\s*]{0,}[$]?\w+)
/* match indices
[0] entire matched string
[1] leading whitespace
[2] the left term
[3] unquoted version of [1] if quoted
[4] a vector if [1] is one
[5] a decimal if [1] is one
[6] an integer if [1] is one
*/
decodeExprn = new RegExp( '(?:[()\\s]*)' + exprnTerm + '(?:\\s*' + reOperators + '\\s*'
+ exprnTerm + ')*(?:[()\\s]*)', 'g' );
// (?:[()\s]*)((?:'([^'\\]*(?:\\'[^'\\]*)*)')|(\[[^[]*\])|([+-]?\d*[.]\d+|[+-]?\d+[.]\d*)|([+-]?\d+)|[!\s*]{0,}[$]?\w+)(?:\s*([=!<>]+)\s*((?:'([^'\\]*(?:\\'[^'\\]*)*)')|(\[[^[]*\])|([+-]?\d*[.]\d+|[+-]?\d+[.]\d*)|([+-]?\d+)|[!\s*]{0,}[$]?\w+))*(?:[()\s]*) // - expression can be a exprnTerm OR exprnTerm op exprnTerm
/* match indices
[0] entire matched string
[1] leading whitespace
[2] the left term
[3] unquoted version of [1] if quoted
[4] a vector if [1] is one
[5] a decimal if [1] is one
[6] an integer if [1] is one
NB: the rest can be undefined if there is no comparison operator
[6] comparison operator
[7] leading whitespace
[8] the right term
[9] unquoted version of [7] if quoted
[10] a vector if [7] is one
[11] a decimal if [7] is one
[12] an integer if [7] is one
*/
decodeAssign = new RegExp( '\\s*([' + prefix + ']?\\w+)\\s*[=](?!=)\\s*([^;\\r\\n]+)(?:[;\\s]|$)*', 'g' );
// \s*([$]?\w+)\s*[=](?!=)\s*([^;\r\n]+)(?:[;\s]|$)*
/*
[0] entire matched string
[1] assignment target (option or hostVar)
[2] expression
*/
// NB: SO_PREFIX = '$O_'
decodeFnCall = new RegExp( '(\\s*)(?:(worldScripts[.]\\w+)[.](\\w+)|([$]O_\\w+)|((?:[' + prefix
+ ']|[$_]+)\\w+))([(].*(?=[)][^)]*(?:;|$))[)])(\\s*)(?:[;\\s]|$)*', 'g' );
// (\s*)(?:(worldScripts[.]\w+)[.](\w+)|([$]O_\w+)|((?:[$]|[$_]+)\w+))([(].*(?=[)][^)]*(?:;|$))[)])(\s*)(?:[;\s]|$)*
/*
[0] entire matched string
[1] leading whitespace
[2] hostOxp name, eg. worldScripts.telescope
[3] function name following [1]
[4] function name of a station_options exposed function call
[5] function name of an hostOxp function call, ie. starts with hostVarPrefix
[6] function parameters
[7] trailing whitespace
*/
}
function parsePages( pages ) {
var pageCount = 0;
for( let idx = 0, len = pages.length; idx < len; idx++ ) {
let page = pages[ idx ];
pageLabels.push( page );
let optnStr = expandText( keyPrefix + page + '_options' );
if( optnStr ) {
pageCount++;
let options = optnStr.match( matchListOfStr );
if( options ) {
parseOptionList( page, options );
} else {
log(so.name, 'parsePages, error parsing "_options" value: ' + optnStr
+ '\n page = ' + page + ', pageCount = ' + pageCount + ', pageLabels = ' + pageLabels
+ '\n expandMissionText called w/ "' + (keyPrefix + page + '_options') + '"' );
}
} else {
log(so.name, 'parsePages, missing "_options" key, page = ' + page
+ ', pageLabels = ' + pageLabels + ', pageCount = ' + pageCount
+ '\n expandMissionText called w/ "' + (keyPrefix + page + '_options') + '"' );
}
}
return pageCount;
}
function parseOptionList( page, optionsList ) {
var that = parseOptionList;
var optionVars = (that.optionVars = that.optionVars || []);
optionVars.length = 0;
for( let idx = 0, len = optionsList.length; idx < len; idx++ ) {
let varName = parseOption( page, optionsList[ idx ] );
if( varName ) // parseOption reports errors
optionVars.push( varName );
}
if( !optionPages.hasOwnProperty( page ) ) {
optionPages[ page ] = getArray();
} else {
optionPages[ page ].length = 0;
}
let dest = optionPages[ page ];
dest.push.apply( dest, optionVars );
// - this adds each element of 'optionVars' to the 'dest' array,
// vs. dest = dest.concat( optionVars );
// - concat returns a NEW array (more garbage)
allOptions.push.apply( allOptions, optionVars );
}
function parseOption( page, option ) {
var varName, optString;
// remove any option strings (vs. variables) from page's '_options'
if( option.indexOf( ':' ) >= 0 ) {
let names = option.split( ':' );
if( names.length !== 2 ) {
let msg = 'parseOption, ERROR: invalid option renaming "' + option + '"';
msg += '\n the correct format is variableName:optionName, ';
msg += '\n where optionName appears on option the pages';
msg += '\n and worldScripts.myOxp[ hostVarPrefix + variableName ] ';
msg += '\n is assigned the value.';
log( so.name, msg );
return;
}
varName = names[ 0 ].trim();
optString = names[ 1 ].trim();
} else {
optString = varName = option.trim();
}
if( expandText( keyPrefix + varName ) === null ) {
log( so.name, 'parseOption, ERROR: missing expansion option "'
+ ( keyPrefix + varName )
+ '"; should be in missiontext.plist or missionKeys' );
return;
}
makeInfo( page, optString, varName );
return varName;
}
function makeInfo( page, optString, varName ) {
try {
if( expandText( keyPrefix + varName ) === null ) {
log( so.name, 'makeInfo, ERROR: missing expansion option "'
+ ( keyPrefix + varName )
+ '"; should be in missiontext.plist or missionKeys' );
return;
}
var result = getObject( infoTemplate );
optionInfo[ varName ] = result;
result.option = varName;
result.optionStr = optString;
result.optionLen = measureWord( optString );
result.page = page;
result.controls = getArray();
result.relyOnOptions = getArray();
result.relyOnHostVars = getArray();
decodeOption( varName, result );
result.currently = getOptionValue( varName );
} catch( err ) {
log( so.name, so._reportError( err, makeInfo, [page, optString, varName] ) );
throw err;
}
}
function createOptionInfo( pages, allOptions ) {
function addToList( value, list ) {
if( list && list.indexOf( value ) < 0 ) {
list.push( value );
}
}
function addToValueList( option, value, info, isHostVar ) {
if( isHostVar ) {
addToList( value, info.relyOnHostVars );
} else {
addToList( value, info.relyOnOptions );
addToList( option, optionInfo[ value ].controls );
// - option that dictates whether other options are shown
}
}
try{
refreshSOKeys();
var foundOne, value, matchStr, leadWS, term, unQuoted, aVector, aDecimal, anInt;
// scan '_condition' for options & hostVars
for( let idx = 0, len = pages.length; idx < len; idx++ ) {
let ignore, nextTerm, page = pages[ idx ];
let options = optionPages[ page ];
for( let opt = 0, optLen = options.length; opt < optLen; opt++ ) {
let option = options[ opt ];
let info = optionInfo[ option ];
let condition = expandText( keyPrefix + option + '_condition' );
if( condition === null )
continue;
info.isConditional = true;
foundOne = false;
decodeTerm.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
while( true ) {
nextTerm = decodeTerm.exec( condition );
/* debugging regex
if( nextTerm !== null ) {
log('\n match found using \n' + decodeTerm + '\n upon \n' + condition + '\nnextTerm:\n' );
let mNames = [ 'matchStr', 'leading', 'term', 'unQuoted', 'aVector', 'aDecimal', 'anInt',
'operator', 'leading', 'rHandSide', 'rUnQuoted', 'rVector', 'rDecimal', 'rInteger' ]
for( let x = 0, len = nextTerm.length; x < len; x++ ) {
log(mNames[x] + '\t\t' + (x < 10 ? ' ' + x : x) + ': ' + nextTerm[x] );
}
}
*/
if( nextTerm === null ) {
if( foundOne )
break;
throw( 'createOptionInfo, "' + option + '", decodeTerm cannot decode condition \n\t"'
+ condition + '"\n\tusing: \n\t' + decodeTerm );
}
[ matchStr, leadWS, term, unQuoted, aVector, aDecimal, anInt ] = nextTerm;
if( term.length > 0 ) {
[ value, ignore ] = splitExclamations( term );
} else {
throw( 'createOptionInfo, decodeTerm match produced empty capture: ' + nextTerm );
}
freeArray( nextTerm );
let isHostVar = startsWith( value, hostVarPrefix ),
isOption = allOptions.indexOf( value ) >= 0;
if( !isHostVar && !isOption ) // ignore true, false, null, etc
continue;
foundOne = true;
addToValueList( option, value, info, isHostVar );
}
}
}
/// _showProps( obj, objName, newLine, show_deep, expand_arrays, show_type )
// log('createOptionInfo, exit' + cd._showProps(optionInfo, 'optionInfo', 1,2,1));
} catch( err ) {
log( so.name, so._reportError(err, createOptionInfo, [pages, allOptions]));
throw err;
}
}
// : ' + + '
var registerFailures = [];
function updateRegistry( prefix ) { // called upon activation of Interface (F4) screen
// had to split into 2 parts as refreshSOKeys() must precede this but follow updatePageData()
registerFailures.length = 0;
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name,
'updateRegistry, missiontext prefix NOT registered: "' + prefix + '"' );
return false;
}
var host = registeredOxps[ prefix ];
hostOxp = host.hostOxp;
// these are alterable vis exposed functions, so registeredOxps is up to date
optionsAllowedCallback = host.optionsAllowedCallback;
callPWSG = host.callPWSG;
notifyCallback = host.notifyCallback;
suppressSummary = host.suppressSummary;
// missionKeys set in initOptions
// as all missiontext keys may be overridden, update related effects
// (hostVarPrefix is in updatePageData())
lowestValue = expandText( prefix + 'lowestValue' ); // is optional, no failure msg
if( !lowestValue || lowestValue.length === 0 ) {
lowestValue = null; // value when turned off or not present
}
highestValue = expandText( prefix + 'highestValue' ); // is optional, no failure msg
if( !highestValue || highestValue.length === 0 ) {
highestValue = null; // value when turned off or not present
}
maxPrecision = expandText( prefix + 'maxPrecision' );
if( !maxPrecision || maxPrecision.length === 0 ) {
maxPrecision = 0; // default value, no failure msg
}
maxVectorLen = expandText( prefix + 'maxVectorLen' );
if( !maxVectorLen || maxVectorLen.length === 0 ) {
maxVectorLen = 0; // default value, no failure msg
}
allow_reset = expandText( prefix + 'allow_reset_of_page' );
if( !allow_reset || allow_reset.length === 0 ) {
allow_reset = false; // default value, no failure msg
} else {
allow_reset = allow_reset === 'yes' || allow_reset === 'true' ? true : false;
}
formatting = expandText( prefix + 'formatting_of_page' );
if( !formatting || formatting.length === 0 ) {
formatting = false; // default value, no failure msg
} else {
formatting = formatting === 'yes' || formatting === 'true' ? true : false;
}
let tabStopsStr = expandText( prefix + 'optionTabStops' );
if( !tabStopsStr || tabStopsStr.length === 0 ) {
registerFailures.push( prefix + 'optionTabStops' );
} else {
optionTabStops.length = 0;
if( !parseTabStops( tabStopsStr ) )
registerFailures.push( prefix + 'optionTabStops' );
}
let interfaceChanged = false;
let title = checkOverride( prefix, 'interface_title' );
if( !title || title.length === 0 ) {
registerFailures.push( prefix + 'interface_title' );
} else {
interfaceChanged = interfaceChanged || title !== host.title;
host.title = title;
}
let summary = checkOverride( prefix, 'interface_summary' );
if( !summary || summary.length === 0 ) {
registerFailures.push( prefix + 'interface_summary' );
} else {
interfaceChanged = interfaceChanged || summary !== host.summary;
host.summary = summary;
}
let category = checkOverride( prefix, 'interface_category' );
if( !category || category.length === 0 ) {
registerFailures.push( prefix + 'interface_category' );
} else {
interfaceChanged = interfaceChanged || category !== host.category;
host.category = category;
}
if( interfaceChanged ) {
setOneInterface( prefix );
}
if( registerFailures.length ) {
log( so.name, 'updateRegistry, failed to expand: ' + registerFailures.join(', ') );
return false;
}
return true;
}
var splitTabStops = /[^{]*[{]\s*([^}]*)\s*[}]\s*[,]?\s*/g;
function parseTabStops( tabStops ) {
var tabStopList = tabStops.split( splitTabStops );
if( tabStopList ) {
for( let idx = 0, len = tabStopList.length || 0; idx < len; idx++ ) {
let tabs = tabStopList[ idx ];
if( !tabs || tabs.length === 0 )
continue;
let lst = tabs.match( matchNumbers );
if( lst ) {
let lastNum = 0, result = getArray();
for( let lx = 0, len = lst.length; lx < len; lx++ ) {
let curr = parseFloat( lst[ lx ] );
if( curr < 0 || (lastNum > 0 && curr <= lastNum) ) { // allow first to be zero
let repl = lastNum + 0.1;
log(so.name, 'parseTabStops, bad tab value "' + curr + '" <= "'
+ lastNum + '" (previous), using "' + repl );
log(so.name, ' (tabs must be increasing in value)' );
curr = repl;
}
result.push( curr );
lastNum = curr;
}
optionTabStops.push( result );
} else {
log(so.name, 'parseTabStops, invalid numbers in "_optionTabStops" key'
+ ', tabs = ' + tabs
+ ', expandMissionText called w/ "' + (keyPrefix + 'optionTabStops') + '"' );
return false;
}
}
} else {
log(so.name, 'parseTabStops, error parsing "_optionTabStops" key'
+ ', tabStops = ' + tabStops
+ ', expandMissionText called w/ "' + (keyPrefix + 'optionTabStops') + '"' );
return false;
}
return true;
}
function expandText( textKey, keys ) {
var that = expandText;
var merged = (that.merged = that.merged || {}); // working map assembled for each text expansion
if( !textKey || textKey.length === 0 )
return null; // expandMissionText returns null if textKey not present
var keyMap = missionKeys,
result = null; // expandMissionText returns null if textKey not present
if( keys ) { // create merger of default and oxp supplied keys
clearObject( merged, true ); // true => keepObjects (ie. del prop only, not what it references)
for( let prop in missionKeys ) {
if( missionKeys.hasOwnProperty( prop ) ) {
merged[ prop ] = missionKeys[ prop ];
}
}
for( let prop in keys ) {
if( keys.hasOwnProperty( prop ) ) {
merged[ prop ] = keys[ prop ];
}
}
keyMap = merged;
}
// missionKeys override; if the key is an empty string, it's treated
// as if it was deleted
var inMissionKeys = missionKeys !== undefined && missionKeys.hasOwnProperty( textKey )
&& missionKeys[ textKey ].length > 0;
if( inMissionKeys ) {
/// need ' || {}' else Oolite crashes
result = expandDescription( missionKeys[ textKey ], keyMap || {} );
} else {
/// need ' || {}' else Oolite crashes
result = expandMissionText( textKey, keyMap || {} );
}
return result;
}
function checkOverride( prefix, prop ) {
// values from missiontext can be overridden using missionKeys
// while those passed at registration are altered via the exposed
// functions (ie. the registeredOxps[ prefix ].prop value is always current)
// - the two sets of values may overlap (eg. interface_title)
var result = null,
host = registeredOxps[ prefix ],
mKey = prefix + prop,
check = expandText( mKey ); // respects missionKeys
if( check !== null ) {
result = check;
} else if( host.hasOwnProperty( prop ) ) {
result = host[ prop ];
}
if( missionKeys.hasOwnProperty( mKey ) && check === null ) {
let msg = 'checkOverride, missionKeys override "' + mKey + '":\n\t';
msg += missionKeys[ mKey ] + '\n\texpansion failed';
if( result ) {
msg += ', reverting to registered value "' + result + '"';
} else {
msg += ', using default (if it exists) otherwise null';
}
log( so.name, msg );
}
return result;
}
// : ' + + '
function refreshSOKeys( soKeys ) {
// called from createOptionInfo, initOptions, nextOptionsPage, processChoice, setChoice, setOptionValue
var that = refreshSOKeys;
var mkKeyProps = (that.mkKeyProps = that.mkKeyProps || []);
mkKeyProps.length = 0;
function refreshSOPageKeys( index ) {
var numPages = pageLabels.length;
missionKeys.stn_optns_page_count = numPages;
if( numPages > 0 && index >= 0 && index < numPages) {
missionKeys.stn_optns_page = pageLabels[ index ];
missionKeys.stn_optns_page_num = index + 1;
missionKeys.stn_optns_next_page_num = (( index + 1 ) % ( numPages || 1 )) + 1;
} else {
missionKeys.stn_optns_page = '';
missionKeys.stn_optns_page_num = 0;
missionKeys.stn_optns_next_page_num = 0;
}
}
if( missionKeys === undefined ) {
log('refreshSOKeys, ERROR: missionKeys === undefined' );
return;
}
missionKeys.stn_optns_clockString = clock.clockStringForTime( clock.adjustedSeconds );
missionKeys.stn_optns_curr_system = system.name;
// the rest are oxp dependent
if( soKeys && soKeys.hasOwnProperty( 'index' ) ) {
refreshSOPageKeys( soKeys.index );
} else if( pageLabels.length > 0 && currentScreenIdx < pageLabels.length ) {
refreshSOPageKeys( currentScreenIdx );
} else {
refreshSOPageKeys( 0 );
}
// missionKeys.stn_optns_option = ??? - handled in processChoice
missionKeys.stn_optns_changes_count = changesMade.hasOwnProperty( 'stnOptsPending' )
? changesMade.stnOptsPending : 0;
}
///////////////////////////////////////////////////////////////////////////////
// formatting functions ///////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
function splitExclamations( term ) {
var count = 0;
if( term[ 0 ] === '!' ) {
// count & strip all leading exclamation marks
let idx, len;
for( idx = 0, len = term.length; idx < len; idx++ ) {
let it = term[ idx ];
if( it === '!' ) {
count++;
} else if( it !== ' ' && it !== '\t' ) {
break;
}
}
term = term.slice( idx );
}
return [ term, count ];
}
function startsWith( term, prefix ) { // String.startsWith not in our JS
return term.slice( 0, prefix.length ) === prefix;
}
function endsWith( term, suffix ) { // String.endsWith not in our JS
return term.slice( -suffix.length ) === suffix;
}
function setOneInterface( prefix ) {
var that = setOneInterface;
var interfaceDefn = (that.interfaceDefn = that.interfaceDefn || {});
clearObject( interfaceDefn );
if( !registeredOxps.hasOwnProperty( prefix ) ) {
log( so.name, 'setOneInterface, missing for prefix "' + prefix + '", aborting' );
return;
}
var host = registeredOxps[ prefix ];
var checkPermission = host.optionsAllowedCallback;
var allowed = checkPermission && typeof checkPermission === 'function'
? checkPermission() : true;
if( allowed ) { // turn it on in F4 screen
interfaceDefn.title = host.interface_title;
interfaceDefn.summary = host.interface_summary;
interfaceDefn.category = host.interface_category;
interfaceDefn.callback = host.callback.bind( so );
if( ps.dockedStation ) // is null when reloading via console & not docked!
ps.dockedStation.setInterface( prefix, interfaceDefn );
} else { // ensures it's off
if( ps.dockedStation )
ps.dockedStation.setInterface( prefix, null );
}
}
function _setInterfaces() { // called from equipmentAdded, equipmentAdded, shipDockedWithStation, etc.,
// to start up interface(s)
ps = player && player.ship;
for( var prefix in registeredOxps ) { // set/clear interface based on callback
if( registeredOxps.hasOwnProperty( prefix ) ) {
setOneInterface( prefix );
} else {
log( so.name, '_setInterfaces, missing for prefix "' + prefix + '", skipping' );
}
}
}
function nextOptionsPage() { // find next array of options using currentScreenIdx
var that = nextOptionsPage;
var soKeys = (that.soKeys = that.soKeys || {});
clearObject( soKeys );
var numPages = pageLabels.length;
currentScreenIdx = ( currentScreenIdx + 1 ) % numPages;
soKeys.index = currentScreenIdx;
refreshSOKeys( soKeys );
// check range in case pageLabels was changed
return currentScreenIdx >= 0 && currentScreenIdx < numPages
? pageLabels[ currentScreenIdx ] : null;
}
// : ' + + '
function displayOptions( via ) { // optional 'via' for choice that got us here
var that = displayOptions;
var screen = (that.screen = that.screen || {}); // re-used for runScreen; kept local to fn as static keys retained
var summary, screenLines;
// moved from initOptions to allow dynamic pages
if( !updatePageData() )
return;
if( currentScreenIdx === undefined ) // got here via initOptions
currentScreenIdx = 0;
refreshSOKeys(); /// relies on pageLabels, which is set in updatePageData
if( !updateRegistry( keyPrefix ) )
return;
var currentScreen = pageLabels[ currentScreenIdx ];
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // 1st time through; this part is static
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = true;
screen.textEntry = false;
}
screen.screenID = 'stn_optns_displayOptions_' + keyPrefix + currentScreen + '_page';
screen.titleKey = keyPrefix + currentScreen + '_title';
summary = expandText( keyPrefix + currentScreen + '_summary' );
if( !summary ) {
log( so.name, so._reportError('missing or invalid "' + keyPrefix + currentScreen
+ '_summary" in missiontext.plist'));
return
}
let lines = measureText( currentScreen + '_summary', summary );
if( formatting ) {
formattedPage.length = 0;
let textInfo = textIndices[ keyPrefix ][ currentScreen + '_summary' ];
formatEols( summary, textInfo, 0, lines );
screen.message = formattedPage.join( '\n' );
} else {
screen.message = summary;
}
screenLines = MAXSCREENLINES - lines; // # lines left after summary
screen.allowInterrupt = changesMade.cag$stnOptsPending === 0;
screen.choices = buildOptions( screenLines, screen, via );
mission.runScreen( screen, processChoice.bind( so ) );
}
// buttons shared by options list and sub-pages
// - the last button on sub-pages, "Return to ...", is dynamic and always recycled
var blankLine, scrollButton, scrollTopButton, nextPgButton,
saveButton, abortButton, exitButton, saveEditButton, resetButton;
function initButtons() { // buttons for options list and sub-pages
var alignment = expandText( 'stn_optns_button_alignment' );
var defaultColor = expandText( 'stn_optns_default_color' );
if( blankLine === undefined ) {
blankLine = getChoice( '' );
}
if( scrollButton === undefined ) {
scrollButton = getChoice( expandText( 'stn_optns_scroll_down' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_scroll_down' ] = scrollButton;
} else {
scrollButton.text = expandText( 'stn_optns_scroll_down' );
scrollButton.color = expandText( 'stn_optns_scroll_down_button_color' ) || defaultColor;
scrollButton.alignment = alignment;
scrollButton.unselectable = false;
}
if( scrollTopButton === undefined ) {
scrollTopButton = getChoice( expandText( 'stn_optns_scroll_top' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_scroll_top' ] = scrollTopButton;
} else {
scrollTopButton.text = expandText( 'stn_optns_scroll_top' );
scrollTopButton.color = expandText( 'stn_optns_scroll_top_button_color' ) || defaultColor;
scrollTopButton.alignment = alignment;
scrollTopButton.unselectable = false;
}
if( nextPgButton === undefined ) {
nextPgButton = getChoice( expandText( 'stn_optns_list_next_page' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_list_next_page' ] = nextPgButton;
} else {
nextPgButton.text = expandText( 'stn_optns_list_next_page' );
nextPgButton.color = expandText( 'stn_optns_list_next_page_button_color' ) || defaultColor;
nextPgButton.alignment = alignment;
nextPgButton.unselectable = false;
}
if( saveButton === undefined ) {
saveButton = getChoice( expandText( 'stn_optns_list_save' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_list_save' ] = saveButton;
} else {
saveButton.text = expandText( 'stn_optns_list_save' );
saveButton.color = expandText( 'stn_optns_list_save_button_color' ) || defaultColor;
saveButton.alignment = alignment;
saveButton.unselectable = false;
}
if( abortButton === undefined ) {
abortButton = getChoice( expandText( 'stn_optns_list_abort' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_list_abort' ] = abortButton;
} else {
abortButton.text = expandText( 'stn_optns_list_abort' );
abortButton.color = expandText( 'stn_optns_list_abort_button_color' ) || defaultColor;
abortButton.alignment = alignment;
abortButton.unselectable = false;
}
if( exitButton === undefined ) {
exitButton = getChoice( expandText( 'stn_optns_list_exit' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_list_exit' ] = exitButton;
} else {
exitButton.text = expandText( 'stn_optns_list_exit' );
exitButton.color = expandText( 'stn_optns_list_exit_button_color' ) || defaultColor;
exitButton.alignment = alignment;
exitButton.unselectable = false;
}
if( saveEditButton === undefined ) {
saveEditButton = getChoice( expandText( 'stn_optns_option_save' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_option_save' ] = saveEditButton;
} else {
saveEditButton.text = expandText( 'stn_optns_option_save' );
saveEditButton.color = expandText( 'stn_optns_option_save_button_color' ) || defaultColor;
saveEditButton.alignment = alignment;
saveEditButton.unselectable = false;
}
if( resetButton === undefined ) {
resetButton = getChoice( expandText( 'stn_optns_option_reset' ) );
missionKeys.stn_optns_buttons[ 'stn_optns_option_reset' ] = resetButton;
} else {
resetButton.text = expandText( 'stn_optns_option_reset' );
resetButton.color = expandText( 'stn_optns_option_reset_button_color' ) || defaultColor;
resetButton.alignment = alignment;
resetButton.unselectable = false;
}
}
function isCommonButton( key ) { // check for static buttons
do {
if( key === 'UU_blank' ) break; // blank line for spacing
if( key === 'XX_reset' ) break; // 'Reset to factory default'
if( key === 'YY_save' ) break; // 'Apply changes and exit', color get toggled
if( key === 'ZZ_abort' ) break; // top level only, 'Exit discarding changes'
if( key === 'ZZ_exit' ) break; // top level only, 'Exit (no changes)'
if( key === 'WW_next_pg' ) break; // top level only, 'Switch to next page'
if( key === 'ZZ_return' ) return false; // specific to sub-page, so always recycled
// not testing 'VV_more' (top level only) or 'ZZ_more' (sub-page only) as require different handling
return false;
} while( false );
return true;
}
function buildOptions( screenLines, screen, via ) {
try {
var that = buildOptions;
var choices = (that.choices = that.choices || null); // re-used for runScreen; kept local to fn as static keys retained
var optionRows = (that.optionRows = that.optionRows || {});
var lastStart = that.lastStart || 0; // index of starting option on page just displayed
var lastEnd = that.lastEnd || 0; // index of last option showing on page just displayed
var lastValid = that.lastValid || 0; // number of valid options on page just displayed
var lastHost = that.lastHost || null;
if( via === 'WW_next_pg' || via === 'XX_reset' || currentChoice.option === null ) {
that.lastStart = that.lastEnd = that.lastValid = 0;
}
var currentScreen = pageLabels[ currentScreenIdx ],
options = optionPages[ currentScreen ],
start, end, tabs, option,
lastValidKey, lastValidShowing = false,
len = options.length, numValid = 0,
lines = screenLines - 1; // # lines for list (-1 for blank line before list)
var haveChanges = changesMade.hasOwnProperty( 'cag$stnOptsPending' );
initButtons();
if( !choices ) { // first time, create static choices
blankLine.unselectable = true;
choices = that.choices = getObject();
choices[ 'UU_blank' ] = blankLine;
choices[ 'YY_save' ] = saveButton;
choices[ 'ZZ_exit' ] = exitButton;
lines -= 3;
} else {
for( var x in choices ) { // re-use objects
if( choices.hasOwnProperty( x ) ) {
if( isCommonButton( x ) ) { // keep, are static
lines--;
} else if( x === 'VV_more' ) { // just delete the reference
delete choices[ x ];
} else {
freeChoice( choices[ x ] );
delete choices[ x ];
}
}
}
}
if( !lastHost || lastHost !== keyPrefix ) { // being run by a different oxp
let hasReset = choices.hasOwnProperty( 'XX_reset' );
if( allow_reset && !hasReset ) {
choices[ 'XX_reset' ] = resetButton;
lines--;
} else if( !allow_reset && hasReset ) {
delete choices[ 'XX_reset' ];
lines++;
}
let hasNextPg = choices.hasOwnProperty( 'WW_next_pg' );
if( pageLabels.length > 1 && !hasNextPg ) {
choices[ 'WW_next_pg' ] = nextPgButton;
lines--;
} else if( pageLabels.length === 1 && hasNextPg ) {
delete choices[ 'WW_next_pg' ];
lines++;
}
that.lastHost = keyPrefix; // so we only do this once, not every call
}
if( haveChanges ) { // changes have been made
delete choices[ 'ZZ_exit' ];
choices[ 'ZZ_abort' ] = abortButton;
saveButton.color = expandText( 'stn_optns_default_color' ) || 'yellowColor';
saveButton.unselectable = false;
} else {
delete choices[ 'ZZ_abort' ];
choices[ 'ZZ_exit' ] = exitButton;
saveButton.color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
saveButton.unselectable = true;
}
tabs = currentScreenIdx >= optionTabStops.length
? null : optionTabStops[ currentScreenIdx ];
// insert ALL valid options into 'validOptions'
lastValidKey = formatOptions( tabs );
numValid = Object.keys( validOptions ).length;
if( numValid > lines ) {
lines--; // for 'more' button
}
start = lastStart; // guess page didn't shift
end = lastEnd > 0 ? lastEnd
: lastStart + lines - 1; // -1 so last option starts next page
let initChKey = via;
if( via === 'WW_next_pg' ) { // first time on page
scroller.reset( numValid );
end = (numValid < lines ? numValid : lines) - 1;// 0 indexed
start = scroller.curr( numValid, lines, end );
} else if( via === 'VV_more' ) { // scroll page
start = scroller.next( numValid, lines );
end = start + lines - 1; // 0 indexed
} else if( currentChoice.option === null ) { // first page of opened F4 interface
if( via === undefined ) {
start = scroller.curr( numValid, lines, end );
}
initChKey = haveChanges ? 'YY_save' : 'WW_next_pg';
} else { // back from editing option
initChKey = currentChoice.option;
}
screen.initialChoicesKey = initChKey;
let delta = lastValid > 0 ? numValid - lastValid : 0;// ?did # options change
if( numValid <= lines ) { // fits on one page
end = numValid - 1; // 0 indexed
start = scroller.curr( numValid, lines, end );
} else if( delta !== 0 ) { // number of options has changed (conditionals)
end = lastStart + (numValid > lines ? lines : numValid) - 1;
// set position so initChKey remains the same, if possible
// ie. when sub-options collapse/expand
if( delta > 0 ) { // option expanded
let row = optionRows[ initChKey ]; // option's place in displayed options
if( row + delta >= lines - 1 ) { // sub-options extend beyond end of page
let valid = Object.keys( validOptions );
let place = valid.indexOf( initChKey ); // option's place in all valid options
end = place + delta; // shift up to see all sub-options
}
} else if( delta < 0 ) { // option collapsed
if( lastEnd + delta > numValid - 1 ) {// not enough remaining options to fill to end of page
end = numValid - 1 - delta;
}
}
start = scroller.curr( numValid, lines, end );
}
that.lastStart = start;
that.lastEnd = end;
that.lastValid = numValid;
clearObject( optionRows );
let skip = 0, // # of VALID options skipped before starting
count = 0; // # of VALID options displayed
for( let idx = 0; idx < len; idx++ ) { // set up option buttons
option = options[ idx ];
if( !validOptions.hasOwnProperty( option ) )
continue;
if( skip++ < start ) // option has scrolled above top of page
continue;
lastValidShowing = option === lastValidKey;
let prefix = count < 10 ? '0' + count : count;
let choiceKey = prefix + '_' + option;
let nextOption = getChoice( validOptions[ option ] )
// setup nextOption for display
nextOption.alignment = 'LEFT';
if( optionInfo[ option ].isSuppressed ) {
// the only suppressed options that are added to validOptions are
// controllers that aren't controlled (ie. top level in dependency)
// and those options solely dependent on oxp vars, not options
nextOption.color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
} else if( option === currentChoice.option ) {
nextOption.color = expandText( 'stn_optns_lastUsed_color' ) || 'greenColor';
} else if( changesMade.hasOwnProperty( option ) ) {
nextOption.color = expandText( 'stn_optns_optionChanged_color' ) || 'cyanColor';
}
if( option === currentChoice.option ) {
screen.initialChoicesKey = choiceKey;
}
optionRows[ option ] = count;
choices[ choiceKey ] = nextOption;
if( ++count >= lines ) break;
}
if( numValid > lines ) { // add 'more' button
if( lastValidShowing ) {
choices[ 'VV_more' ] = scrollTopButton;
} else {
choices[ 'VV_more' ] = scrollButton;
}
if( screen.initialChoicesKey === 'VV_more' ) {
choices[ 'VV_more' ].color = expandText( 'stn_optns_lastUsed_color' ) || 'greenColor';
}
}
return choices;
} catch( err ) {
log( so.name, so._reportError( err, buildOptions, [screenLines, via] ) );
throw err;
}
}
// : ' + + '
function checkCondition( option ) {
var that = checkCondition;
var invalid = (that.invalid = that.invalid || [ false, 0 ]);
var indents = (that.indents = that.indents || []);// .length only array for .join() for dependency markers
indents.length = 0;
var line = '', inserted = 0;
var conditionKey = keyPrefix + option + '_condition';
var info = optionInfo[ option ];
var conditions = expandText( conditionKey );
var subsShowing = expandText( 'stn_optns_subOptions_showing' ),
subsHidden = expandText( 'stn_optns_subOptions_hidden' ),
subsMarker = expandText( 'stn_optns_subOptions_marker' );
var showingLen = measureWord( subsShowing ),
hiddenLen = measureWord( subsHidden );
info.condition = conditions;
if( conditions ) {
info.isSuppressed = !conditionMet( option, conditionKey, conditions );
do {
if( !info.isSuppressed ) break;
// don't suppress top level controllers
if( info.controls.length > 0 && info.relyOnOptions.length === 0
&& info.relyOnHostVars.length === 0)
break;
// or those that rely solely on hostVars
if( info.relyOnHostVars.length !== 0 && info.relyOnOptions.length === 0 )
break;
return invalid;
} while( false );
}
var indentIt = null;
if( info.controls.length > 0 ) { // prefix option to show whether it controls other options
let currVal = getOptionValue( option );
let controlStr = currVal ? subsShowing : subsHidden,
controlLen = currVal ? showingLen : hiddenLen;
line = controlStr;
inserted += controlLen;
indentIt = false;
}
/* looks too busy -> allow option entry but all buttons disabled
if( info.relyOnHostVars.length > 0 ) {
// prepend option with '$' to indicate # hostVar dependencies
indents.length = info.relyOnHostVars.length + 1;
let hostVarStr = indents.join( '$' );
line += hostVarStr; // mark option as hostVar dependent
inserted += strFontLen( hostVarStr );
indentIt = indentIt !== false ? true : false;
}
*/
let reliesUpon = info.relyOnOptions; // list, may be empty
let indentStr = '';
if( reliesUpon.length > 0 ) {
// check all parent options are present
let maxCtrlStrLen = 0;
for( let idx = 0, len = reliesUpon.length; idx < len; idx++ ) {
let parent = reliesUpon[ idx ];
if( !startsWith( parent, hostVarPrefix ) && !optionInfo.hasOwnProperty( parent ) ) {
log(so.name, 'checkCondition, ERROR, "' + option + '" relies on "'
+ parent + '" which is missing! conditions: ' + conditions );
continue;
}
// set indent to size of parent's prefix
let pHidden = optionInfo[ parent ].isSuppressed;
let pCtrlStrLen = pHidden ? hiddenLen : showingLen;
if( pCtrlStrLen > maxCtrlStrLen ) {
maxCtrlStrLen = pCtrlStrLen;
}
}
// prepend option with colons to indicate # option dependencies
indentStr = paddingText( '', maxCtrlStrLen );
indents.length = reliesUpon.length + 1;
let optionStr = indents.join( subsMarker );
line += optionStr; // mark option as conditional
inserted += strFontLen( optionStr );
indentIt = indentIt !== false ? true : false;
}
line = (indentIt ? indentStr : '') + line + info.optionStr;
return [ line, inserted ];
}
function formatOptions( tabs ) {
// check which options are valid; find max column shift
var that = formatOptions;
var valid = (that.valid = that.valid || []);
var lines = (that.lines = that.lines || []);
var shiftedTabs = (that.shiftedTabs = that.shiftedTabs || []);
valid.length = 0;
lines.length = 0;
shiftedTabs.length = 0;
var currentScreen = pageLabels[ currentScreenIdx ],
options = optionPages[ currentScreen ];
var idx, len, option, line, insert, maxInsert = 0;
for( idx = 0, len = options.length; idx < len; idx++ ) {// find all non-suppressed options
option = options[ idx ];
// returns start of line (option, special char?), length of any special char
[ line, insert ] = checkCondition( option );
if( line === false ) { // option is suppressed
continue;
}
if( insert > maxInsert ) {
maxInsert = insert;
}
valid.push( option );
lines.push( line );
}
// shift tabs to account for inserted markers
for( idx = 0, len = tabs.length; idx < len; idx ++ ) {
shiftedTabs[ idx ] = tabs[ idx ] + maxInsert;
}
clearObject( validOptions );
for( idx = 0, len = valid.length; idx < len; idx++ ) {// format valid options
option = valid[ idx ];
line = lines[ idx ];
line += shiftedTabs && shiftedTabs.length > 0
? paddingText( line, shiftedTabs[ 0 ] ) : ''; // '\t\t'
validOptions[ option ] = formatLine( option, line, shiftedTabs );
}
return valid[ valid.length - 1 ]; // last valid option
}
// : ' + + '
function formatLine( option, line, tabs ) {
var value = getOptionValue( option );
var type = typeof value;
// space for middle column, ie. option's value
var gap = tabs && tabs.length > 1
? tabs[ 1 ] - tabs[ 0 ] : 0;
// short description
var brief = expandText( keyPrefix + option );
if( brief && brief.length > 0 ) {
let endSpec = brief.indexOf( ')' );
if( endSpec > 0 )
brief = brief.slice( endSpec + 1 ).trim(); // skip past type, default & range(?)
}
if( value === null ) {
line += 'null';
} else if( type === 'undefined' ) {
line += ''; // blank for page of only text
} else if( type === 'string' ) {
line += value;
} else if( type === 'boolean' ) {
line += expandText( ( value ? 'stn_optns_boolean_true' : 'stn_optns_boolean_false' ) );
} else if( type === 'number' ) {
let num = floor( value ) === value // an integer
? value : value.toPrecision( maxPrecision < 1 ? 1 : maxPrecision );
let precision = maxPrecision - 1;
while( gap > 0 && precision >= 1 && measureWord( '' + num ) > gap ) { // overflows the gap
num = value.toPrecision( precision );
precision--;
}
line += num;
} else if( Array.isArray( value ) || value instanceof Vector3D ) {
line += formatArray( option, value, line, brief, tabs );
} else { // something else
line += '{object}';
if( !changesMade.hasOwnProperty( option ) ) {// an array is only created when getOptionValue from host
freeObject( value ); // else we're using the one in changesMade
}
}
// assemble
line += tabs && tabs.length > 1
? paddingText( line, tabs[ 1 ] ) : '\t';
line += brief;
while( strFontLen( line ) > MAXSCREENWIDTH ) { // truncate long lines on word boundary
line = line.slice( 0, line.lastIndexOf( ' ' ) );
}
return line;
// return getChoice( line );
}
function formatArray( option, value, line, brief, tabs ) {
// return string representing the array/vector value of 'key'
// so it fits within tab stops
var that = formatArray;
var items = (that.items = that.items || []);
items.length = 0;
function getItems( precision ) {
items.length = 0;
for( let idx = 0; idx < len; idx++ ) {
let item = value[ idx ];
if( item % 1 === 0 ) { // remove .0's
items.push( item );
} else {
// .toPrecision may return exponential notation
let precise = item.toPrecision( precision );
let pStr = precise.toString();
let eIdx = pStr.indexOf( 'e' );
if( eIdx >= 0 ) {
let dot = pStr.indexOf( '.' );
if( dot < 0 ) {
dot = eIdx;
}
precise = dot >= precision ? item.toFixed( 0 )
: item.toFixed( precision - dot );
}
items.push( precise );
}
}
}
var gap = tabs && tabs.length > 1
? tabs[ 1 ] - tabs[ 0 ] : 0;
var fmtStr, valueF, len = value.length,
typeStr = Array.isArray( value ) ? '{array}' : '{vector}',
vectorLen = maxVectorLen, result = '';
var hairSpace = String.fromCharCode( 0x200a );
getItems( vectorLen );
if( gap > 0 ) {
if( !vectorLen || vectorLen < 1 ) {
// !vectorLen is zero when user turns it off: default is to round to integer
// vectorLen = floor(maxPrecision / len + 0.5); // round( maxPrecision / len )
vectorLen = 1; // argument to .toPrecision must be >= 1
}
while( true ) { // try to squeeze value into gap
fmtStr = items.join( ', ' );
valueF = measureWord( fmtStr );
if( valueF < gap ) // not <= so there's a space to next column
break;
fmtStr = items.join( ',' + hairSpace );
valueF = measureWord( fmtStr );
if( valueF < gap ) // not <= so there's a hairSpace to next column
break;
fmtStr = items.join( ',' );
valueF = measureWord( fmtStr );
if( valueF <= gap ) // butts up against next column
break;
vectorLen--;
if( vectorLen < 1 )
break;
getItems( vectorLen );
}
result = valueF > gap ? typeStr : fmtStr;
} else { // gap == 0 => no tabStops specified
let lineF = measureWord( line ),
briefF = measureWord( brief );
fmtStr = items.join( ', ' );
valueF = measureWord( fmtStr );
if( lineF + valueF + briefF > MAXSCREENWIDTH ) {
fmtStr = value.join( ',' + hairSpace );
valueF = measureWord( fmtStr );
}
if( lineF + valueF + briefF > MAXSCREENWIDTH ) {
fmtStr = value.join( ',' );
valueF = measureWord( fmtStr );
}
result = lineF + briefF + valueF > MAXSCREENWIDTH ? typeStr : fmtStr;
}
if( !changesMade.hasOwnProperty( option ) ) { // an array is only created when getOptionValue from host
freeArray( value ); // else are using the one in changesMade
}
return result;
}
///////////////////////////////////////////////////////////////////////////////
// evaluation functions ///////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// : ' + + '
var classifiedTerm = {};
function classifyTerm( term, option, context ) {
var that = classifyTerm;
var exclamations = (that.exclamations = that.exclamations || []);
// check if 'term' is an option or hostVar & fetch its value
// - return quoted strings as will be used in eval calls
var count, value = term, type = null, opposite = false;
if( term.length === 0 ) {
term = value = "''";
return [ term, value, type, opposite ];
}
[ term, count ] = splitExclamations( term );
if( count > 0 ) {
exclamations.length = count + 1;
opposite = exclamations.join( '!' );
}
if( startsWith( term, SO_PREFIX ) ) {
type = so.hasOwnProperty( term ) && typeof so[ term ] === 'function'
? 'soFunction' : 'unknown';
} else if( term === so.name ) {
type = 'soFunction';
} else if( term === 'worldScripts' ) {
type = 'oxpFunction';
} else if( startsWith( term, hostVarPrefix ) ) {
if( !hostOxp.hasOwnProperty( term ) ) {
type = 'hostVariable'
log( so.name, 'classifyTerm, for "' + option
+ '", term "' + term + '" not found in: '
+ keyPrefix + ', assuming it is a variable\n\t' + context );
} else if( typeof so[ term ] === 'function' ) {
type = 'hostFunction';
} else {
type = 'hostVariable';
value = getOptionValue( stripPrefix( term ), true ); // true => get from hostOxp
if( value === undefined ) {
log( so.name, 'classifyTerm, for "' + option
+ '", "' + term + '" is undefined: ' + context );
}
}
} else {
let page;
if( optionInfo.hasOwnProperty( term ) ) {
page = optionInfo[ term ].page;
}
if( page ) { // it's an option
optionInfoToObj( term, classifiedTerm );
type = classifiedTerm.type;
if( classifiedTerm.error ) {
type = 'unknown'; // checkValue will have logged error
}
value = getOptionValue( term );
if( value === undefined ) {
log( so.name, 'classifyTerm, for "' + option
+ '", option "' + term + '" is undefined: ' + context );
} else {
value = checkValue( value, classifiedTerm, option );
}
} else if( global.hasOwnProperty( term ) ) {
type = typeof global[ term ] === 'function' ? 'ooFunction' : 'unknown';
}
}
if( typeof value === 'string' && value.length === 0 ) {
value = "''";
}
return [ term, value, type, opposite ];
}
var termSplitter = /\s*(?:[()]*)([=<>&~^|&?:]+|[!][=]{1,2})\s*/; // exclude solo '!' as stays with term
var matchCompareOp = /\s*([=!<>]+)\s*/;
// NB: global flag 'g' must not be used as it's for successive matches only
// (.exec remember index from previous use; set re's .lastIndex=0 if must use /g)
var indexingArray = /'\s*([[][^\]]*[\]])\s*''\s*([[][^\]]*[\]])\s*'/g;
function substituteValues( string, option, assignKey ) {
var that = substituteValues;
var delayed = (that.delayed = that.delayed || []);
function valueFromString( term, unquoted, vector, decimal, integer ) {
var value;
if( integer ) value = parseInt( integer, 10 );
else if( decimal ) value = parseFloat( decimal );
else if( vector ) value = JSON.parse( vector );
else if( term === 'null' ) value = null;
else if( term === 'undefined' ) value = undefined;
else if( term === 'true' ) value = true;
else if( term === 'false' ) value = false;
else if( unquoted ) {
// return quoted strings as will be used in eval calls
if( /\\'/.test( term ) ) { // remove escapes for escaped quotes;
value = '"' + unquoted.replace( /\\'/g, "'" ) + '"';
} else {
value = isSingleQuoted( term ) ? term : "'" + stripQuotes( term ) + "'";
}
} else { // some other unquoted string (?option or hostVar)
value = term;
}
return value;
}
function indexInList( list, item ) {
var addQuotes = isSingleQuoted( item );
for( let idx = 0, len = list.length; idx < len; idx++ ) {
let it = list[ idx ];
if( item === (addQuotes ? "'" + it + "'" : it) ) {
return idx;
}
}
return -1;
}
function valueToType( value, type, expression ) {
function indexError( kind ) {
return 'valueToType, ' + kind + ' (' + coerced + ') for value "' + value
+ '" out of range for option "' + classifiedTerm.option
+ '", \n\tmust be in interval 0..' + (choices.length - 1)
+ ' for ' + kind + ' values: ' + choices
+ '\n\tsee "' + option + '": ' + expression;
}
var choices, coerced = value;
if( type === 'hostVariable' ) {
if( Array.isArray( value ) ) {
coerced = valueToType( value, 'vector', expression );
} else if( typeof value === 'string' ) {
coerced = valueFromString( value, stripQuotes( value ) )
}
} else if( type === 'choice' && typeof value === 'string' ) {
choices = classifiedTerm.selection; // ASSUMES prior call to classifyTerm
if( !choices || choices.length === 0 )
throw( 'valueToType, cannot find choices for: ' + classifiedTerm.option
+ '; see "' + option + '": ' + expression );
coerced = indexInList( choices, value );
if( coerced < 0 || coerced >= choices.length ) {
throw indexError( 'choice' );
}
} else if( type === 'vector' ) { // validate term's elements
let isArray = Array.isArray( value );
if( !isArray || value.length === 0 ) {
throw( 'valueToType, ' + (isArray ? 'empty' : 'missing') + ' array for "'
+ classifiedTerm.option + '"; see "' + option + '": ' + expression );
}
for( let vi = 0, len = value.length; vi < len; vi++ ) {
let item = value[ vi ];
let rangeChk = valueInRange( item, classifiedTerm );
if( typeof rangeChk === 'string' )
// not between min & max or if no range in spec., not between lowestValue & highestValue
throw( 'valueToType, invalid value ( ' + item + ' ) for "'
+ classifiedTerm.option + '" : ' + rangeChk
+ '; see "' + option + '": ' + expression );
}
/* convert arrays to string for JS compare (JS has no inherent array comparison)
(JSON.stringify'd arrays have [], .toString does not)
- no benefit using JSON as condition strings are first converted to
numbers, so insignificant differences won't matter (eg. -0, trailing zeros)
*/
// use single quotes to be consistent with strings which are always single quoted
coerced = "'[" + value.toString() + "]'";
} else if( type === 'bitflags' && typeof value === 'string' ) {
choices = classifiedTerm.selection; // ASSUMES prior call to classifyTerm
if( !choices || choices.length === 0 )
throw( 'valueToType, cannot find bitflag strings for: ' + classifiedTerm.option
+ '; see "' + option + '": ' + expression );
coerced = indexInList( choices, value );
if( coerced < 0 || coerced >= choices.length ) {
throw indexError( 'bitflagStrs' );
}
} else if( type === 'number' || type === 'decimal' || type === 'bitflags' ) {
let rangeChk = valueInRange( value, decodedChoice );
if( typeof rangeChk === 'string' )
// not between min & max or if no range in spec., not between lowestValue & highestValue
throw( 'valueToType, invalid value ( ' + value + ' ) for "'
+ classifiedTerm.option + '": ' + rangeChk
+ '; see "' + option + '": ' + expression );
} else if( type === 'toggle' ) {
if( typeof value === 'string' ) {
let choices = classifiedTerm.selection;// ASSUMES prior call to classifyTerm
if( !choices || choices.length === 0 )
throw( 'valueToType, cannot find choices for: ' + classifiedTerm.option
+ '; see "' + option + '": ' + expression );
let idx = indexInList( choices, value );
coerced = idx === 0 ? false : true;
}
} // no validation for 'text'
return coerced;
}
function optionValue( matchStr, leadWS, term, unQuoted, aVector, aDecimal, anInt, offset, origStr ) {
var that = optionValue;
var previous = (that.previous = that.previous ||
{ 'term': null, 'value': null, 'unQuoted': null, 'type': null, 'operator': null });
var numTerms = (that.numTerms = that.numTerms || -1);// -1 as numTerms++ for each, indices are 0-based
var context = origStr.slice( offset );
matchCompareOp.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
var operator = matchCompareOp.exec( previous.operator );
operator = operator ? operator[ 1 ] : null; // coerceValue only uses it for messages
var [ term, value, type, opposite ] = classifyTerm( term, option, context );
if( type === null ) {
value = valueFromString( term, unQuoted, aVector, aDecimal, anInt );
if( typeof value === 'string' ) {
if( matchCompareOp.test( previous.operator ) ) {
value = valueToType( value, previous.type, context );
} else if( assignKey ) {
if( optionInfo.hasOwnProperty( assignKey ) ) {
optionInfoToObj( term, classifiedTerm );
value = valueToType( value, optionInfo[ assignKey ].type, context );
}
}
} else if( Array.isArray( value ) ) {
value = valueToType( value, 'vector', context );
}
} else if( type !== 'unknown' && !endsWith( type, 'Function' ) ) {
value = valueToType( value, type, context );
if( type !== 'hostVariable' ) {
if( previous.type === null && operator && previous.unQuoted ) {
// missed coercion as option trails string
delayed.push( [ numTerms - 1, // -1 due to the operator's that.numTerms++;
valueToType( previous.value, type, context ) ] );
}
}
}
that.numTerms++;
that.previous.term = term;
that.previous.value = value;
that.previous.unQuoted = unQuoted;
that.previous.type = type;
termSplitter.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
let trailing = termSplitter.exec( origStr.slice( offset + matchStr.length ) );
if( trailing ) { // operator after term, could be comparison or logical
that.previous.operator = trailing[ 1 ];
that.numTerms++;
} else {
that.previous.operator = null;
}
return leadWS + ( opposite ? opposite : '') + value;
}
delayed.length = 0;
optionValue.numTerms = -1;
let result = string.replace( decodeTerm, optionValue );
if( delayed.length ) {
let splat = result.split( termSplitter );
// .split can have empty strings, eg. when space between termSplitter char.s
let terms = splat.filter( function( item ) { return item !==''; } );
for( let idx = 0, len = delayed.length; idx < len; idx++ ) {
let [ count, target ] = delayed[ idx ];
terms[ count ] = target;
}
result = terms.join( ' ' );
}
// for an array index, remove doubled single quotes
result = result.replace( indexingArray, '$1$2' );
return result;
}
// : ' + + '
function conditionMet( option, conditionKey, conditions ) {
try {
decodeExprn.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
var substituted = substituteValues( conditions, conditionKey );
if( startsWith( option, 'AutoScan' ) ) {
log('conditionMet, conditions: ' + conditions );
log(' substituted: ' + substituted );
}
try {
// use strict protects the global namespace
let result = eval( "'use strict'; " + substituted );
return result;
} catch( err ) {
log( so.name, 'conditionMet caught ' + err + ' on:'
+ '\n eval( >>' + substituted + '<< );'
+ '\n where the argument was derived via substitution from:'
+ '\n ' + conditions
+ '\n in "' + conditionKey + '"' );
return true; // don't let error suppress option
}
} catch( err ) {
// FYI: can throw object, existing (or constructed in throw statement), eg. conditionalOption
if( typeof err === 'string' ) { // one of mine
if( err.length ) { // zero length string => error already issued
log( so.name, 'conditionMet, ' + err );
}
return true; // don't let error suppress option
} else
log( so.name, so._reportError( err, conditionMet, [ option, conditionKey, conditions ] ) );
throw err;
}
}
// : ' + + '
function doAssignments( option ) {
var assignKey = keyPrefix + option + '_assign';
var expanded = expandText( assignKey );
if( !expanded )
return;
try {
var search, foundOne, result, index, lastIndex;
decodeAssign.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
var statement, substituted, expression,
statements = expanded.split( /[;]/ );
foundOne = false;
index = lastIndex = 0;
for( let idx = 0, len = statements.length; idx < len; idx++ ) {
expression = '';
statement = statements[ idx ];
if( statement.length === 0 )
continue;
decodeAssign.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
search = decodeAssign.exec( statement );
if( search === null ) { // not an assignment statement
if( foundOne )
break;
throw( 'doAssignments, unable to parse _assign specifier for "'
+ option + '" : ' + statement + '\n\t_assign: ' + expanded );
}
index = search.index;
let [ matchStr, target, assignment ] = search;
if( index > lastIndex ) {
log( so.name, 'doAssignments, invalid assignment statement: "' + statement
+ ', nothing should precede the target : "' + target + '"' );
continue;
}
foundOne = true;
let value, optType, isNot;
[ target, value, optType, isNot ] = classifyTerm( target, assignKey, matchStr );
substituted = substituteValues( assignment, assignKey, target );
lastIndex = index;
expression += 'worldScripts.' + hostOxp.name + '.'
+ (optType === 'hostVariable' ? '' : hostVarPrefix)
+ target + ' = ' + substituted;
freeArray( search );
try{
result = eval( "'use strict'; " + expression );
if( missionKeys.hasOwnProperty( target ) ) {
missionKeys[ target ] = getOptionValue( target );
}
} catch( err ) {
log( so.name, 'doAssignments caught ' + err + ' on:'
+ '\n eval( ' + expression + ' );'
+ '\n for "' + assignKey );
}
if( optType !== null ) {
let option = stripPrefix( target );
let startVal = getOptionValue( option, true );// true => from hostOxp
if( changesMade.hasOwnProperty( option ) && changesMade[ option ] !== startVal ) {
log( so.name, 'doAssignments, WARNING: ' + assignKey + ' overwrites "' + option
+ '" that player changed from: ' + startVal + ' to: ' + changesMade[ option ]
+ '; its valus is now: ' + result );
}
removeChangeMade( option ); // now rendered moot
}
}
} catch( err ) {
log( so.name, so._reportError( err, doAssignments, option ) );
if( typeof err === 'string' ) {
log( so.name, err );
} else {
throw err;
}
}
}
function doExecutes( option ) {
var executeKey = keyPrefix + option + '_execute';
var expanded = expandText( executeKey );
if( !expanded )
return;
try {
var search, foundOne, result, index, lastIndex;
decodeFnCall.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
var statement, substituted, expression,
statements = expanded.split( /[;]/ );
for( let idx = 0, len = statements.length; idx < len; idx++ ) {
statement = statements[ idx ];
if( statement.length === 0 )
continue;
decodeAssign.lastIndex = 0; // reset re's .lastIndex as it doesn't reset on a new string!!!!
let assign = decodeAssign.test( statement );
if( assign ) {
log( so.name, 'doExecutes, assignment detected: ' + statement
+ '\n\tIgnoring as it will not work due to complete value substitution. Move it into an "_assign" option');
continue;
}
substituted = substituteValues( statement, executeKey );
foundOne = false;
expression = '';
index = lastIndex = 0;
while( true ) {
search = decodeFnCall.exec( substituted );
if( search === null ) { // no function calls
if( foundOne )
break;
throw( 'doExecutes, unable to parse "'
+ executeKey + '" : '
+ substituted.slice( lastIndex )
+ '\n\t_execute: ' + expanded );
}
index = search.index;
let [ matchStr, leading, wsName, wsFn, soFn, oxpFn, args, trailing ] = search;
foundOne = true;
expression += substituted.slice( lastIndex, index );
lastIndex = index;
if( wsName && wsFn ) {
let script = eval( wsName );
if( script !== undefined ) { // oxp is present
if( script.hasOwnProperty( wsFn )
&& typeof script[ wsFn ] === 'function' ) {
expression += leading + wsName + '.'
+ wsFn + args + trailing;
index += matchStr.length;
} else {
log( so.name, 'doExecutes, unable to locate function "'
+ wsFn + '"in "' + executeKey + '" : '
+ substituted.slice( index )
+ '\n\t_execute: ' + expanded );
}
} else {
log( so.name, 'doExecutes, unable to locate worldScripts "'
+ wsName + '"in "' + executeKey + '" : '
+ substituted.slice( index )
+ '\n\t_execute: ' + expanded );
}
} else if( soFn ) {
if( so.hasOwnProperty( soFn )
&& typeof so[ soFn ] === 'function' ) {
expression += leading + 'worldScripts.' + so.name
+ '.' + soFn + args + trailing;
index += matchStr.length;
} else {
log( so.name, 'doExecutes, unable to locate ' + so.name + ' function "'
+ soFn + '"in "' + executeKey + '" : '
+ substituted.slice( index )
+ '\n\t_execute: ' + expanded );
}
} else if( oxpFn ) {
if( hostOxp.hasOwnProperty( oxpFn )
&& typeof hostOxp[ oxpFn ] === 'function' ) {
expression += leading + 'worldScripts.' + hostOxp.name
+ '.' + oxpFn + args + trailing;
index += matchStr.length;
} else {
log( so.name, 'doExecutes, unable to locate global function "'
+ oxpFn + '"in "' + executeKey + '" : '
+ substituted.slice( index )
+ '\n\t_execute: ' + expanded );
}
} else {
throw( 'doExecutes, unable to decode "' + executeKey + '": ' + matchStr );
}
}
try{
result = eval( "'use strict'; " + expression ); // + ';'
} catch( err ) {
log( so.name, 'doExecutes caught ' + err + ' on:'
+ '\n eval( ' + expression + ' );'
+ '\n for "' + executeKey + '", after substituting values in:'
+ '\n ' + expanded );
}
freeArray( search );
}
} catch( err ) {
log( so.name, so._reportError( err, doExecutes, option ) );
if( typeof err === 'string' ) {
log( so.name, err );
} else {
throw err;
}
}
}
// : ' + + '
///////////////////////////////////////////////////////////////////////////////
// processing functions ///////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
function optionInfoToObj( option, obj ) {
// clearObject( obj, true ); // true -> keepObjects
for( let prop in obj ) { // reset to defaults (infoTemplate)
if( obj.hasOwnProperty( prop ) ) {
if( infoTemplate.hasOwnProperty( prop ) ) {
obj[ prop ] = infoTemplate[ prop ];
} else {
delete obj[ prop ];
}
}
}
if( optionInfo.hasOwnProperty( option ) ) {
var info = optionInfo[ option ];
for( let prop in info ) {
if( info.hasOwnProperty( prop ) ) {
obj[ prop ] = info[ prop ];
}
}
obj.currently = getOptionValue( option );
}
}
var keyFromChoice = /^(\d\d)_(\w+)/; // parse choice key: NN_<variable name>
function processChoice( choice ) { // for an optionPages selection
try {
// log('processChoice, ===> ' + cd._showProps(choice, 'choice',0,1));
var currentScreen = pageLabels[ currentScreenIdx ];
refreshSOKeys();
missionKeys.stn_optns_option = null;
clearObject( currentChoice ); // re-use object
if( !choice || choice === 'ZZ_exit' || choice === 'ZZ_abort' ) {
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references)
clearObject( missionKeys.stn_optns_changed );
_purgepools();
return;
} else if( choice === 'YY_save' ) { // button will be unselectable if no changes
saveChoices();
_purgepools();
return;
} else if( choice === 'XX_reset' ) {
confirmReset();
return;
} else if( choice === 'WW_next_pg' ) {
currentChoice.option = null;
scroller.reset();
currentScreen = nextOptionsPage();
if( currentScreen !== null )
displayOptions( choice );
return;
} else if( choice === 'VV_more' || choice === 'ZZ_more' ) {
currentChoice.option = null;
displayOptions( choice );
return;
}
var changing = choice.match( keyFromChoice );
if( changing === null ) {
log( so.name, 'processChoice, ERROR: failed to decode "' + choice + '" using: ' + keyFromChoice );
return;
}
var option = changing[ 2 ]; // get option from choice
// var option = currentChoice.option = changing[ 2 ];// get option from choice
freeArray( changing );
var options = optionPages[ currentScreen ];
if( options.indexOf( option ) < 0 ) {
log( so.name, 'processChoice, ERROR: cannot find option: ' + option + ' in ' + options );
} else {
optionInfoToObj( option, currentChoice );
missionKeys.stn_optns_option = option;
scroller.start();
let type = currentChoice.type;
if( type === 'toggle' ) chooseBoolean();
else if( type === 'number' ) chooseNumber();
else if( type === 'vector' ) chooseVector();
else if( type === 'decimal' ) chooseNumber(); // function handles both int & float
else if( type === 'bitflags' ) chooseBits();
else if( type === 'choice' ) chooseChoice();
else /* assume text */ chooseText();
}
} catch( err ) {
log( so.name, so._reportError( err, processChoice, choice, 1 ) );
throw err;
}
}
// parsing of the brief option specifier is done in stages, subsequent stages using the rest
var decodeType = /\(\s*(text|toggle|number|decimal|vector|bitflags|choice)\s*(,?)\s*(.*?\))/;
/*
[1] the type
[2] a comma, maybe
[3] the rest, including closing parenthesis
*/
var decodeDefault = /\s*(null|true|false|\{.*?\}|'([^'\\]*(?:\\.[^'\\]*)*)'|[-+.\w]*)\s*(,?)\s*(.*?\))/;
/*
[1] default value
[2] unquoted version of [1] if quoted
[3] a comma, maybe
[4] the rest, including closing parenthesis
*/
var decodeRange = /\s*(\{.*?\}|([-+.\w]*)\s*(:)\s*([-+.\w]*))\s*\)/;
/*
[1] selection enclosed in braces {} if [3] is not a colon
[2] minimum if [3] is a colon
[3] a colon, maybe
[4] maximum if [3] is a colon
*/
/// - decodeOption needs a 'do not check' arg to suppress calls to checkValue, as only needs doing
/// when option is first decoded *OR*
/// build a map of maps of decoded options (ie. cache) to use for rest of game <- easier
function decodeOption( option, destination ) { // for option definitions
var matchStr, unquoted, type, comma, rest, defawlt,
descr = expandText( keyPrefix + option ),
items = descr ? descr.match( decodeType ) : null;
if( !items ) {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', failed to decode (bad type) "' + descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
return;
}
[matchStr, type, comma, rest] = items;
destination.option = option;
destination.type = type;
// set selection if applicable (done here as any default is optional)
if( type === 'bitflags' ) {
let flags = getBitFlagStrs( option ) || getArray();
destination.selection = flags;
let numBits = flags.length, allBits = 0;
for( let x = 0; x < numBits; x++ )
allBits |= 1 << x;
destination.numBits = numBits;
destination.allBits = allBits;
} else if( type === 'toggle' ) {
destination.selection = getArray( expandText( 'stn_optns_boolean_false' ),
expandText( 'stn_optns_boolean_true' ) );
}
if( !comma ) { // no more, ie. no default, range
if( type === 'text' ) { // no comma => text only page
destination.type = 'display';
} else if( type === 'bitflags' || type === 'decimal' || type === 'number' ) {
destination.defawlt = 0; // allowed to skip both default & range
destination.min = destination.max = null;
} else if( type === 'choice' ) {
destination.defawlt = 0; // default to 1st in list if no default specified
} else if( type === 'toggle' ) {
destination.defawlt = false;
} else if( type === 'vector' ) {
destination.defawlt = getArray( 0, 0, 0 );
} else {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', incomplete type spec. (expecting comma) "' + descr + '"';
log( so.name, 'decodeOption, ' + error );
}
return;
}
items = rest.match( decodeDefault ); // no error checking as always succeeds
[matchStr, defawlt, unquoted, comma, rest] = items;
defawlt = defawlt.replace( /\\'/g, "'" ); // unescaped imbedded quotes
if( comma && (type === 'text' || type === 'toggle') ) {
let error = 'Error: for option ' + (keyPrefix + option) + ', ' + type
+ ' cannot have a range "' + descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
return;
}
destination.defawlt = defawlt = defawlt ? checkValue( defawlt, destination, option ) : null;
destination.min = destination.max = null;
if( !comma ) {
return; // no comma => no more to parse
}
items = rest.match( decodeRange );
if( !items ) {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', missing range or too many commas in "'+ descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
return;
}
comma = items[ 3 ];
if( comma === ':' ) { // we have a range
getRange( option, items[ 2 ], items[ 4 ], destination );
} else if( type === 'choice' ) {
destination.selection = parseArray( items[ 1 ], type, option );
if( typeof defawlt === 'number' &&
(defawlt < 0 || defawlt >= destination.selection.length) ) {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', default index (' + defawlt + ') not in list "'
+ descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
} else if( typeof defawlt === 'string' ) {
let index = destination.selection.indexOf( defawlt );
if( index < 0 ) {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', default (' + defawlt + ') not in list "'
+ descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
} else { // convert one of choices into an index
destination.defawlt = index;
}
}
} else {
let error = 'Error: for option ' + (keyPrefix + option)
+ ', invalid type (' + type + ') for choice list "'
+ descr + '"';
log( so.name, 'decodeOption, ' + error );
destination.error = error;
}
}
function getBitFlagStrs( option ) {
var flags = null,
bitflagStrs = expandText( keyPrefix + option + '_bitflagStrs' );
if( bitflagStrs === null ) {
log( so.name, 'getBitFlagStrs, ERROR: missing or invalid _bitflagStrs key for "' + option + '"' );
} else {
flags = bitflagStrs.match( matchListOfStr );
if( !flags || flags.length === 0 ) {
flags = null;
}
}
return flags;
}
var parseNumber = /^\s*([+-]?[0-9]+)\s*$/;
var parseDecimal = /^\s*([+-]?([0-9]+[.]?[0-9]*|[.][0-9]+))\s*$/;
var quoteStripper = /'([^'\\]*(?:\\.[^'\\]*)*)'/;
var dblQuoteStripper = /"([^"\\]*(?:\\.[^"\\]*)*)"/;
var quoteTrimmer = /(^')|('$)/g;
function checkValue( checking, result, option, orType ) {
// used for both validating missiontext.plist & user input
// !now also for validating _condition terms
// - errors go to log file, setChoice mk's its own msgs
// - 'orType' overrides soType for recursive type checking (choice)
var value, soType = orType || result.type,
type = typeof checking;
// log('checkValue, option: ' + option + ', checking: ' + checking + ', soType: ' + soType + ', type: ' + type );
try {
if( checking === undefined
|| (Array.isArray(checking) && checking.length === 0) ) {
value = undefined;
} else if( checking === 'null' || checking === null ) {
value = null;
} else if( soType === 'choice' ) { // 1 level recursion for typing
// must precede next block else may be treated as text (if checking is text)
if( type === 'string' ) {
let match = checking.match( parseNumber );
if( match === null ) {
value = checkValue( checking, element, option, 'text' );
} else {
value = checkValue( checking, element, option, 'number' );
}
} else if( type === 'number' ) {
value = checking;
} else {
throw( expandText( 'stn_optns_bad_entry' ) + ' for choice: ' + value + ', typeof: ' + typeof value );
}
} else if( soType === 'text' ) { /// || type === 'string' - catches vectors
if( checking[ 0 ] === "'" ) { // quotes optional, verify they match
let match = checking.match( quoteStripper );
if( match === null ) {
throw expandText( 'stn_optns_bad_quotes' );
}
value = match[ 1 ];
} else {
value = checking;
}
} else if( soType === 'toggle' || type === 'boolean' ) {
value = checking;
if( typeof value === 'string' ) {
value = checking.replace( quoteTrimmer, '' ); // strip quotes (allow user leeway)
value = value === 'true' ? true :
value === 'false' ? false : 'error';
if( value === 'error' ) {
throw expandText( 'stn_optns_bad_bool' );
}
}
} else if( soType === 'bitflags' ) { // must precede number, as bitflag is a number!
value = parseInt( checking, 10 );
if( value !== value ) {
throw expandText( 'stn_optns_bad_num' );// test for NaN which is never equal to itself
}
if( value < 0 && value !== -1 ) {
throw expandText( 'stn_optns_bad_bits' );
}
} else if( soType === 'number' || soType === 'decimal' || type === 'number' ) {
if( typeof checking === 'string' ) {
let match = checking.match( soType === 'number' ? parseNumber
: parseDecimal );
if( match === null )
throw expandText( 'stn_optns_parse_err' );
value = soType === 'number' ? parseInt( match[ 1 ], 10 )
: parseFloat( match[ 1 ] );
} else {
value = checking;
}
if( value !== value ) {
throw expandText( 'stn_optns_bad_num' ); // test for NaN which is never equal to itself
}
} else if( soType === 'vector' || Array.isArray( checking ) ) {
if( soType === 'vector' && type === 'string' ) {
value = parseArray( checking, soType, option );
} else {
value = checking;
}
if( !value )
throw expandText( 'stn_optns_bad_entry' );
let ball = '';
for( let idx = 0, len = value.length; idx < len; idx++ ) {
if( value[ idx ] !== value[ idx ] ) // test for NaN which is never equal to itself
ball += (ball.length > 0 ? ', ' : '') + expandText( 'stn_optns_bad_array' ) + idx;
}
if( ball.length > 0 ) {
freeArray( value );
throw ball;
}
} else {
throw( expandText( 'stn_optns_bad_type' ) + ' ' + soType );
}
} catch( err ) {
if( err instanceof Error ) { // not thrown by me
log( so.name, so._reportError( err, checkValue, [checking, result, orType], 1 ) );
throw err;
}
let error = 'Error: ' + (err ? err : expandText( 'stn_optns_bad_type' ) + ' ' + soType)
+ ' for ' + (keyPrefix + option) + ', checking: ' + checking;
result.error = error;
log( so.name, 'checkValue, ' + error );
return soType; // returning type indicates to caller an error occured
}
if( result.error ) {
result.error = '';
}
// log('checkValue, exit, returning value: ' + value + ', typeof value: ' + (Array.isArray(value) ? 'array' : typeof value) );
return value;
}
var replaceBraces = /^\s*\{\s*|\s*\}\s*$/g;
var choiceSplitter = /\s*,\s*/;
var element = {}; // tmp obj for results of checkValue
function parseArray( str, type, option ) {
if( !str || str.length === 0 )
return null;
var stripped = str.replace( replaceBraces, '' );
if( !stripped )
return null;
var array, result;
if( type === 'choice' ) {
array = stripped.split( choiceSplitter );
} else {
array = stripped.match( matchNumbers );
}
if( !array || array.length === 0 ) {
return null;
}
result = getArray();
if( type === 'choice' ) {
element.type = type;
for( let idx = 0, len = array.length; idx < len; idx++ ) {
let value = checkValue( array[ idx ], element, option );
if( element.error ) {
log( so.name, 'Error parsing choice = ' + array[ idx ] );
freeArray( result );
return null;
} else { // value could be zero
result.push( value );
}
}
} else {
for( let idx = 0, len = array.length; idx < len; idx++ ) {
result.push( parseFloat( array[ idx ] ) );
}
}
return result;
}
function getRange( option, min, max, result ) {
var type = result.type;
if( !min && !max ) { // are '' if regex found nothing
result.min = result.max = null;
return;
}
let isNumber = type === 'number' || type === 'bitflags';
result.min = min = !min || min.length === 0 ? lowestValue : // lowestValue is null when turned off
isNumber ? parseInt( min, 10 ) : parseFloat( min );
result.max = max = !max || max.length === 0 ? highestValue :// highestValue is null when turned off
isNumber ? parseInt( max, 10 ) : parseFloat( max );
if( type === 'bitflags' ) { // check range is valid
if( min < 0 || max < 0 ) {
log( so.name, 'getRange, Error: found negative bitflag, using '
+ (min < 0 ? 'zero for min, ' : '')
+ (max < 0 ? -max + ' for max, ' :'')
+ ' for ' + (keyPrefix + option) + ', range: ' + min + ':' + max );
if( min < 0 ) {
result.min = min = 0;
}
if( max < 0 ) {
result.max = max = -max;
}
}
if( max !== null ) {
let numBits = result.numBits = max.toString( 2 ).length;
let boundary = pow( 2, numBits ) - 1; // 2^N - 1
if( max !== boundary ) {
result.max = max = boundary;
log( so.name, 'getRange, Error: incorrect bitflag boundary, using max = '
+ max + ', for ' + (keyPrefix+ option) + ', range: ' + min + ':' + max );
}
}
}
}
function formatRange( withTabs ) {
var min = currentChoice.min,
max = currentChoice.max;
return min === null && max === null ? null :
(min !== null ? min : '') + (withTabs ? '\t\t' :'') + ':'
+ (withTabs ? '\t\t' :'') + (max !== null ? max : '');
}
function mkChoicePage( rtnobj, screen, choices, btn_count, initialChoice, errmsg ) {
var that = mkChoicePage;
var depends = (that.depends = that.depends || []);
depends.length = 0;
var option = currentChoice.option,
type = currentChoice.type,
dstr, defawlt = currentChoice.defawlt,
cstr, currently = currentChoice.currently,
range = formatRange( true ),
isSuppressed = optionInfo[ option ].isSuppressed;
if( type === 'vector' ) {
cstr = currently && currently.join( ',\t\t' );
dstr = defawlt && defawlt.join( ',\t\t' );
} else if( type === 'toggle' ) {
cstr = currently === true ? expandText( 'stn_optns_boolean_true' )
: currently === false ? expandText( 'stn_optns_boolean_false' ) : currently;
dstr = defawlt === true ? expandText( 'stn_optns_boolean_true' )
: defawlt === false ? expandText( 'stn_optns_boolean_false' ) : defawlt;
} else if( type === 'choice' ) {
let selection = currentChoice.selection;
cstr = typeof currently === 'number' ? selection[ currently ] : currently;
dstr = typeof defawlt === 'number' ? selection[ defawlt ] : defawlt;
} else {
cstr = currently;
dstr = defawlt;
}
let src = expandText( keyPrefix + option + '_long' );
let textEntry = !isSuppressed && type !== 'toggle' && type !== 'bitflags' && type !== 'choice';
if( textEntry ) {
if( defawlt !== undefined ) src += '\n\n' + expandText( 'stn_optns_enter_plus' );
src += '\n\n' + expandText( 'stn_optns_enter_abort' );
if( type === 'text' ) src += '\n\n' + expandText( 'stn_optns_enter_empty_str' );
}
let status = '\n\n' + expandText( 'stn_optns_setting_str' ) + ': ' + cstr
+ (range ? '\t\t\t\t' + expandText( 'stn_optns_range_str' ) + ':\t\t' + range : '')
+ (defawlt === undefined ? '' : '\t\t\t\t' + expandText( 'stn_optns_default_long' ) + ':\t\t'
+ (defawlt === null ? 'null' : dstr));
if( measurePhrase( status ) > MAXSCREENWIDTH ) { // break up into separate lines
status = '\n\n' + expandText( 'stn_optns_setting_str' ) + ': ' + cstr
+ (range ? '\n' + expandText( 'stn_optns_range_str' ) + ':\t\t' + range : '')
+ (defawlt === undefined ? '' : '\n' + expandText( 'stn_optns_default_str' ) + ':\t\t'
+ (defawlt === null ? 'null' : dstr));
}
let controlStr = '', dependStr = '', info = optionInfo[ option ];
if( info.controls.length > 0 ) {
controlStr = 'This option restricts ' + info.controls.join( ', ' ) + '.';
}
if( info.relyOnOptions.length > 0 ) {
depends.push.apply( depends, info.relyOnOptions );
// - like extend in Python, this adds each element
// of 'relyOnOptions' array to the 'depends' array, vs
// depends = depends.concat( info.relyOnOptions );
// - this returns a NEW array (more garbage)
}
if( info.relyOnHostVars.length > 0 ) {
depends.push.apply( depends, info.relyOnHostVars );
}
if( depends.length ) {
dependStr += 'This option depends on ' + depends.join( ', ' ) + '.';
}
if( controlStr.length || dependStr.length ) {
controlStr = '\n\n(' + controlStr + (controlStr ? ' ': '');
controlStr += dependStr + ')';
}
src += status + controlStr + (errmsg ? '\n\n' + errmsg : '');
scrollText( rtnobj, src, option, // input screens have 1 button; either 'more' button or input line
btn_count + 1, // + 1 for button below
!!errmsg ); // errmsg makes endOfPage true, keeping user @ bottom of page
screen.message = rtnobj.message;
let rtn_key = rtnobj.choice_key;
choices[ rtn_key ] = rtnobj.choice_value; // choices are ignored when textEntry is true
screen.textEntry = textEntry && rtn_key !== 'ZZ_more';
if( isSuppressed ) {
// top level/externally dependent options will still be listed
// - make all but the exit button unselectable
screen.initialChoicesKey = rtn_key;
for( let choice in choices ) {
if( choices.hasOwnProperty( choice ) ) {
if( choice !== rtn_key ) {
choices[ choice ].color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
choices[ choice ].unselectable = true;
}
}
}
} else {
screen.initialChoicesKey = initialChoice || rtn_key;
}
}
// : ' + + '
// object for return values of scrollText, used by all the _choose* fns via mkChoicePage()
var rtnobj = {};
function resetOptionChoices( choices ) {
initButtons(); // update button text for oxp override
var defaultColor = expandText( 'stn_optns_default_color' ) || 'yellowColor';
for( var x in choices ) { // re-use choices object
if( choices.hasOwnProperty( x ) ) {
choices[ x ].unselectable = false;
choices[ x ].color = defaultColor;
if( isCommonButton( x ) ) { // are common buttons, always present
continue;
}
if( x !== 'ZZ_more' ) { // is set after scrollText
freeChoice( choices[ x ] );
}
delete choices[ x ];
}
}
}
function chooseText( errmsg ) {
var that = chooseText;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
var option = currentChoice.option,
type = currentChoice.type;
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // first time, create static part
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
screen.screenID = 'stn_optns_chooseText_' + keyPrefix + option;
if( type === 'display' ) {
let title = expandText( keyPrefix + option );
screen.title = title.slice( title.indexOf( ')' ) + 1 ).trim(); // skip over option type parameters
let src = expandText( keyPrefix + option + '_long' );
scrollText( rtnobj, src, option, 1 ); // 1 for either 'more' or return button
let choice_key = rtnobj.choice_key;
choices[ choice_key ] = rtnobj.choice_value;
screen.message = rtnobj.message;
screen.initialChoicesKey = choice_key;
screen.textEntry = false;
screen.allowInterrupt = changesMade.cag$stnOptsPending === 0;
} else {
expandKeys.stn_optns_option_key = option;
screen.title = expandText( 'stn_optns_choose_text', expandKeys );
mkChoicePage( rtnobj, screen, choices, 1, null, errmsg ); // initialChoice is null
screen.allowInterrupt = false;
}
mission.runScreen( screen, setChoice.bind( so ) );
}
function chooseBoolean() {
var that = chooseBoolean;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
var option = currentChoice.option,
lastUsedColor = expandText( 'stn_optns_lastUsed_color' ) || 'greenColor';
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // first time, create static stuff
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = false;
screen.textEntry = false;
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
screen.screenID = 'stn_optns_chooseBoolean_' + keyPrefix + option;
// expand everytime to allow oxp override
choices[ 'AA_True' ] = getChoice( expandText( 'stn_optns_boolean_true' ) );
choices[ 'BB_False' ] = getChoice( expandText( 'stn_optns_boolean_false' ) );
if( currentChoice.currently === true ) {
choices[ 'AA_True' ].color = lastUsedColor;
} else if( currentChoice.currently === false ) {
choices[ 'BB_False' ].color = lastUsedColor;
}
expandKeys.stn_optns_option_key = option;
screen.title = expandText( 'stn_optns_choose_boolean', expandKeys );
mkChoicePage( rtnobj, screen, choices, 2, null ); // initialChoice is null
mission.runScreen( screen, setChoice.bind( so ) );
}
function chooseNumber( errmsg ) {
var that = chooseNumber;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
var option = currentChoice.option,
range = formatRange();
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // 1st time through; this part is static
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = false;
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
screen.screenID = 'stn_optns_chooseNumber_' + keyPrefix + option;
expandKeys.stn_optns_option_key = option;
expandKeys.stn_optns_number_range = range ? range : '';
screen.title = expandText( (range !== null ? 'stn_optns_choose_number_range'
: 'stn_optns_choose_number'), expandKeys );
mkChoicePage( rtnobj, screen, choices, 1, null, errmsg ); // initialChoice is null
mission.runScreen( screen, setChoice.bind( so ) );
}
function chooseVector( errmsg ) {
var that = chooseVector;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
var option = currentChoice.option;
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // 1st time through; this part is static
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = false;
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
screen.screenID = 'stn_optns_chooseVector_' + keyPrefix + option;
expandKeys.stn_optns_option_key = option;
screen.title = expandText( 'stn_optns_choose_vector', expandKeys );
mkChoicePage( rtnobj, screen, choices, 1, null, errmsg ); // initialChoice is null
mission.runScreen( screen, setChoice.bind( so ) );
}
function chooseBits( value, current, statusMsg ) {
var that = chooseBits;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
var option = currentChoice.option,
defawlt = currentChoice.defawlt,
min = currentChoice.min,
max = currentChoice.max,
buttons, hasDefault = defawlt !== undefined,
currently = value === undefined ? currentChoice.currently : value;
var notInUseColor = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor',
invalidColor = expandText( 'stn_optns_invalid_color' ) || 'redColor',
lastUsedColor = expandText( 'stn_optns_lastUsed_color' ) || 'greenColor';
var clearAll = expandText( 'stn_optns_bitflag_clear_all' ),
setAll = expandText( 'stn_optns_bitflag_set_all' );
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // first time, create static part
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = false;
screen.textEntry = false;
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
expandKeys.stn_optns_option_key = option;
var flags = currentChoice.selection;
var initialChoice = null;
if( !flags ) { // author forgot bitflagStrs (error already reported)
screen.title = expandText( 'stn_optns_choose_number', expandKeys );
mkChoicePage( rtnobj, screen, choices, (hasDefault ? 4 : 3), initialChoice, statusMsg );
mission.runScreen( screen, setChoice.bind( so ) );
return;
}
var idx, len = flags.length,
allBits = currentChoice.allBits,
numBits = currentChoice.numBits;
choices[ 'WB_clear_all' ] = getChoice( clearAll );
if( currently === 0 ) {
choices[ 'WB_clear_all' ].color = notInUseColor;
choices[ 'WB_clear_all' ].unselectable = true;
}
choices[ 'WA_set_all' ] = getChoice( setAll );
if( currently === allBits ) {
choices[ 'WA_set_all' ].color = notInUseColor;
choices[ 'WA_set_all' ].unselectable = true;
}
screen.screenID = 'stn_optns_chooseBits_' + keyPrefix + option;
let changed = false;
if( value !== undefined ) {
let curr_val = getOptionValue( option );
changed = !equalValue( value, curr_val );
curr_val = freeValue( curr_val );
}
buttons = setCommonButtons( option, changed, hasDefault, choices, true );
screen.title = expandText( 'stn_optns_choose_bitflags', expandKeys );
if( numBits === undefined ) { // needed for 'set all flags' cmd
currentChoice.numBits = numBits = len; // - may be overridden by range
}
if( defawlt === -1 ) {
currentChoice.defawlt = allBits;
}
if( min === null ) { // no min in range if supplied
currentChoice.min = min = 0;
}
if( max === null ) { // no max in range if supplied
currentChoice.max = max = allBits;
}
var clearFlag = expandText( 'stn_optns_bitflag_clear' ),
setFlag = expandText( 'stn_optns_bitflag_set' );
for( idx = 0; idx < len; idx++ ) {
let flag = 1 << idx;
let choice = getChoice( flags[ idx ] + ': '
+ (currently & flag ? setFlag : clearFlag) );
let unselectable = flag < min || flag > max;
if( unselectable ) {
choice.color = invalidColor;
} else if( currently & flag ) {
choice.color = lastUsedColor;
} else {
choice.color = notInUseColor;
}
choice.unselectable = unselectable;
let choice_key = (idx < 10 ? '0' + idx : idx) + '_' + currently + '_' + flag;
choices[ choice_key ] = choice;
if( idx < numBits && current === flag ) {
initialChoice = choice_key;
}
}
if( typeof current === 'string' && current.indexOf( '_' ) >= 0 ) { // choice was not an individual bit flag
initialChoice = current === 'WA_set_all' ? 'WB_clear_all' :
current === 'WB_clear_all' ? 'WA_set_all' : current;
}
mkChoicePage( rtnobj, screen, choices, --idx + (hasDefault ? 4 : 3), // idx + 3 or 4 buttons
initialChoice, statusMsg );
mission.runScreen( screen, setChoice.bind( so ) );
}
function setCommonButtons( key, needsSave, hasDefault, choices, inclBlank ) {
// for buttons common to chooseBits & chooseChoice
var buttons = 0;
choices[ 'YY_save' ] = saveEditButton;
if( needsSave ) {
saveEditButton.color = expandText( 'stn_optns_default_color' ) || 'yellowColor';
saveEditButton.unselectable = false;
} else {
saveEditButton.color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
saveEditButton.unselectable = true;
}
buttons++;
if( hasDefault ) { // add reset to default button
choices[ 'XX_reset' ] = resetButton;
buttons++;
} else {
delete choices[ 'XX_reset' ];
}
if( inclBlank ) {
choices[ 'UU_blank' ] = blankLine;
buttons++;
} else {
delete choices[ 'UU_blank' ];
}
return buttons; // mkChoicePage will add 1 for the return/more button it adds
}
function chooseChoice( value, item, listNum, statusMsg ) {
var that = chooseChoice;
var choices = (that.choices = that.choices || {}); // re-used for runScreen; kept local to fn as static keys retained
var screen = (that.screen = that.screen || {}); // "
// args are all undefined when first enter page
var option = currentChoice.option,
defawlt = currentChoice.defawlt,
curr_btn = listNum === undefined ? currentChoice.currently : parseInt( listNum, 10 ),
buttons, hasDefault = defawlt !== undefined,
currently = value === undefined ? currentChoice.selection[ currentChoice.currently ] : value,
lastUsedColor = expandText( 'stn_optns_lastUsed_color' ) || 'greenColor';
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // first time, create static part
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.allowInterrupt = false;
screen.textEntry = false;
screen.choices = choices;
} else {
resetOptionChoices( choices );
}
screen.screenID = 'stn_optns_chooseChoice_' + keyPrefix + option;
let changed = false;
if( value !== undefined ) { // process user's selection
let curr_val = getOptionValue( option );
let index = currentChoice.selection.indexOf( value );
changed = !equalValue( index, curr_val );
curr_val = freeValue( curr_val );
}
buttons = setCommonButtons( option, changed, hasDefault, choices, true ); // true adds blank line after choices
expandKeys.stn_optns_option_key = option;
screen.title = expandText( 'stn_optns_choose_choice', expandKeys );
let items = currentChoice.selection,
idx, len = items.length;
let initialChoice = null;
for( idx = 0; idx < len; idx++ ) {
let selection = items[ idx ];
let choice = getChoice( typeof selection === 'string' && selection.length === 0 ? "''" :
selection === null ? 'null' : selection );
let choice_key = (idx < 10 ? '0' + idx : idx) + '_' + currently + '_' + selection + '_' + idx;
choices[ choice_key ] = choice;
if( idx === curr_btn ) {
choice.color = lastUsedColor;
}
if( listNum !== undefined && idx === curr_btn ) {
initialChoice = choice_key;
}
}
if( item === 'XX_reset' || item === 'YY_save' ) { // choice was not an item
initialChoice = item;
}
mkChoicePage( rtnobj, screen, choices, len + buttons, initialChoice, statusMsg );
mission.runScreen( screen, setChoice.bind( so ) );
}
function setChoice( choice ) { // for selection made in an option's specific page
var that = setChoice;
var vector = (that.vector = that.vector || []);
var errmsg = (that.errmsg = that.errmsg || '');
vector.length = 0;
try {
var currValue = that.currValue;
var parse, value = null,
option = currentChoice.option,
type = currentChoice.type,
defawlt = currentChoice.defawlt,
currently = currentChoice.currently,
isSuppressed = optionInfo[ option ].isSuppressed;
// log('setChoice, ===> ' + cd._showProps(choice, 'choice',0,1));
refreshSOKeys();
switch( true ) {
case choice === 'ZZ_more':
if( type === 'text' || type === 'display' ) chooseText( errmsg );
else if( type === 'toggle' ) chooseBoolean();
else if( type === 'number' || type === 'decimal' ) chooseNumber( errmsg );
else if( type === 'vector' ) chooseVector( errmsg );
else if( type === 'bitflags' ) chooseBits();
else if( type === 'choice' ) chooseChoice();
else
log( so.name, 'setChoice, unknown type = ' + type );
return;
case isSuppressed:
break;
case choice === 'null':
setOptionValue( option, null );
break;
case !choice || choice.length === 0:
that.errmsg = errmsg = '';
scroller.stop();
displayOptions( 'ZZ_abort' );
return;
case choice === '+':
case type === 'text':
if( choice === '+' && defawlt !== undefined ) {
resetOptionValue( option );
break;
}
that.errmsg = errmsg = '';
if( choice === "''" ) {
setOptionValue( option, '' );
break;
}
if( choice.indexOf( "'" ) >= 0 ) { // ensure they match
let quotes = choice.match( quoteStripper );
if( quotes === null ) {
that.errmsg = errmsg = expandText( 'stn_optns_bad_match_quote' ) + " '";
chooseText( errmsg );
return;
}
}
if( choice.indexOf( '"' ) >= 0 ) { // ensure they match
let quotes = choice.match( dblQuoteStripper );
if( quotes === null ) {
that.errmsg = errmsg = expandText( 'stn_optns_bad_match_quote' ) + ' "';
chooseText( errmsg );
return;
}
}
setOptionValue( option, choice );
break;
case type === 'display':
scroller.stop();
displayOptions( 'ZZ_return' );
return;
case type === 'toggle':
if( choice === 'AA_True' && currently !== true ) {
setOptionValue( option, true );
} else if( choice === 'BB_False' && currently !== false ) {
setOptionValue( option, false );
}
break;
case type === 'number' || type === 'decimal':
that.errmsg = errmsg = '';
value = checkValue( choice, currentChoice, option );
if( currentChoice.error ) {
let reason;
if( currentChoice.error.indexOf( expandText( 'stn_optns_parse_err' ) ) >= 0 ) {
if( choice.match( parseDecimal ) ) {
reason = expandText( 'stn_optns_decimal_not_number' );
} else {
reason = expandText( 'stn_optns_bad_type' ) + ' ' + type;
}
}
that.errmsg = errmsg = expandText( 'stn_optns_bad_entry' )
+ ' "' + choice + '", ' + reason;
chooseNumber( errmsg );
return;
}
value = valueInRange( value, currentChoice );
if( typeof value === 'string' ) {
that.errmsg = errmsg = value;
chooseNumber( errmsg );
return;
}
setOptionValue( option, value );
break;
case type === 'vector':
that.errmsg = errmsg = '';
value = checkValue( choice, currentChoice, option );
if( currentChoice.error ) {
freeArray( value );
that.errmsg = errmsg = expandText( 'stn_optns_bad_entry' ) + ' "' + choice
+ '", ' + expandText( 'stn_optns_bad_type' ) + ' ' + type;
chooseVector( errmsg );
return;
}
for( let idx = 0, len = value.length; idx < len; idx++ ) {
let test = valueInRange( value[ idx ], currentChoice );
if( typeof test === 'string' ) {
errmsg += (errmsg.length > 0 ? '\n' : '') + test;
} else {
vector.push( test );
}
}
freeArray( value );
if( errmsg.length > 0 ) {
that.errmsg = errmsg;
chooseVector( errmsg );
return;
}
setOptionValue( option, getArray( vector ) );
break;
case type === 'bitflags':
parse = choice.split( '_' ); // choices encoded as index_value_flag
let bits = parseInt( parse[ 1 ], 10 ),
flag = parseInt( parse[ 2 ], 10 ),
min = currentChoice.min,
max = currentChoice.max;
freeArray( parse );
if( choice === 'WA_set_all'
|| choice === 'WB_clear_all' ) { // can't use -1; only set known bits
flag = choice;
if( currValue === undefined ) {
currValue = getOptionValue( option );
}
let startBit = min ? min.toString(2).length - 1 : 0;
let stopBit = max ? max.toString(2).length
: currentChoice.numBits.toString(2).length;
for( let idx = startBit; idx < stopBit; idx++ ) {
let bit = 1 << idx;
currValue = choice === 'WA_set_all' ? currValue | bit
: currValue & ~bit;
}
} else if( choice === 'YY_save' ) {
setOptionValue( option, currValue );
delete that.currValue; // reset for next option's use
break;
} else if( choice === 'XX_reset' ) {
flag = choice;
currValue = defawlt;
} else if( choice === 'ZZ_return' ) {
delete that.currValue; // reset for next option's use
break;
} else {
currValue = bits ^ flag;
}
that.currValue = currValue;
chooseBits( currValue, flag, expandText( 'stn_optns_curr_flags' ) + ': ' + currValue );
return;
case type === 'choice':
// choices encoded as index_value_item_btn# (value can have spaces!)
parse = choice.split( '_' );
let item = parse[ 2 ],
num = parse[ 3 ];
freeArray( parse );
if( choice === 'YY_save' ) {
// 'number' => assumed to be index into choices
if( typeof currValue === 'number' ) {
setOptionValue( option, currValue );
} else {
setOptionValue( option, currentChoice.selection.indexOf( currValue ) );
}
delete that.currValue; // reset for next option's use
break;
} else if( choice === 'ZZ_return' ) {
delete that.currValue; // reset for next option's use
break;
} else if( choice === 'XX_reset' ) {
item = choice;
if( typeof defawlt === 'number' ) { // defawlt can be either index or one of the choices
currValue = defawlt;
} else {
currValue = currentChoice.selection.indexOf( defawlt );
}
} else {
currValue = item;
// as choices can be defined as list of string or list of number,
// use currentChoice.selection to coerce value
if( typeof currentChoice.selection[0] === 'number' ) {
// value always arrives as string, as it's encoded in button name
currValue = currValue.indexOf('.') == -1
? parseInt( currValue, 10 ) : parseFloat( currValue );
}
}
that.currValue = (typeof currValue === 'string'
&& currValue.length === 0 ? "''" : currValue);
chooseChoice( currValue, item, num,
expandText( 'stn_optns_curr_choice' ) + ': ' +
(typeof currValue === 'number' ? currentChoice.selection[ currValue ]
: currValue) );
return;
}
that.errmsg = errmsg = '';
scroller.stop();
displayOptions();
} catch( err ) {
log( so.name, so._reportError( err, setChoice, choice ) );
throw err;
}
}
function valueInRange( value, choiceObj ) {
var min = choiceObj.min,
max = choiceObj.max;
var error = 'range';
do {
if( min !== null && value < min )
break;
if( max !== null && value > max )
break;
error = 'lowest';
if( min === null && lowestValue !== null && value < lowestValue )
break;
error = 'highest';
if( min === null && highestValue !== null && value > highestValue )
break;
return value;
} while( false );
let msg = expandText( 'stn_optns_bad_range_prefix' ) + ' "' + value + '" ';
if( error === 'range' ) {
msg += expandText( 'stn_optns_bad_range_suffix' ) + ' ' + (min || '') + ' : ' + (max || '');
} else if( error === 'lowest' ) {
msg += expandText( 'stn_optns_bad_range_lowest' ) + ' ' + lowestValue;
} else if( error === 'highest' ) {
msg += expandText( 'stn_optns_bad_range_highest' ) + ' ' + highestValue;
}
return msg;
}
///////////////////////////////////////////////////////////////////////////////
// option & page scrolling ////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// : ' + + '
var pageTemplate = { 'start': -1, 'count': -1, 'lastShown': false };
function Scroller() {
this.pageInfo = [];
}
Scroller.prototype.start = function() {
this.pageInfo.push( getObject( pageTemplate ) );
// this._report('start');
}
Scroller.prototype.stop = function() {
freeObject( this.pageInfo.pop() );
// this._report('stop');
}
Scroller.prototype.reset = function( count ) {
for( let idx = 0, len = this.pageInfo.length; idx < len; idx++ ) {
freeObject( this.pageInfo[ idx ] );
}
this.pageInfo.length = 0;
this.pageInfo.push( getObject( pageTemplate ) );
if( count !== undefined )
this.pageInfo[ 0 ].count = count; // preserve # options as it's dynamic
// this._report('reset');
}
Scroller.prototype._restart = function( count ) {
var level = this.pageInfo.length - 1;
this.pageInfo[ level ].start = 0;
this.pageInfo[ level ].count = count; // preserve # options as it's dynamic
this.pageInfo[ level ].lastShown = false;
// this._report('_restart');
return 0; // convenience return
}
Scroller.prototype._bottomJustify = function( lines, count ) {
var level = this.pageInfo.length - 1;
var start = count <= lines ? 0 : count - lines;
this.pageInfo[ level ].start = start < 0 ? 0 : start;
this.pageInfo[ level ].count = count; // preserve # options as it's dynamic
this.pageInfo[ level ].lastShown = true;
// this._report('_bottomJustify');
return start; // convenience return
}
Scroller.prototype.curr = function( count, lines, see ) {
var level = this.pageInfo.length - 1;
var start = this.pageInfo[ level ].start;
if( start < 0 ) { // 1st time through
return this._restart( count );
}
if( start >= count ) { // page shrunk
return this._bottomJustify( lines, count );
}
// set start so see is visible
var new_start = start;
if( see < start ) { // make see the first line
new_start = see;
} else if( start + lines <= see ) { // make see the final line
new_start = see - lines + 1;
}
if( new_start < 0 ) {
new_start = 0;
// this._report('curr');
} else if( new_start + lines >= count ) { // mixing 0-based indices and 1-based amounts
// this._report('curr');
return this._bottomJustify( lines, count );
}
this.pageInfo[ level ].start = new_start;
this.pageInfo[ level ].count = count;
this.pageInfo[ level ].lastShown = new_start + lines > count - 1;
// this._report('curr');
return new_start;
}
Scroller.prototype.incr = function() {
var level = this.pageInfo.length - 1;
var start = this.pageInfo[ level ].start;
this.pageInfo[ level ].start = start + 1;
// this._report('incr');
}
Scroller.prototype.decr = function() {
var level = this.pageInfo.length - 1;
var start = this.pageInfo[ level ].start;
this.pageInfo[ level ].start = start > 0 ? start - 1 : 0;
// this._report('decr');
}
Scroller.prototype.next = function( count, lines, endOfPage ) {
var level = this.pageInfo.length - 1;
var start = this.pageInfo[ level ].start;
var new_start;
if( start < 0 || count <= lines ) { // 1st time on page OR fits w/o scrolling
new_start = 0;
} else if( endOfPage ) { // stay at bottom
return this._bottomJustify( lines, count );
} else if( this.pageInfo[ level ].lastShown ) { // displayed bottom, return to top
return this._restart( count );
} else { // count > lines, must scroll
new_start = start + lines; // next page
if( new_start + lines >= count ) { // mixing 0-based indices and 1-based amounts
return this._bottomJustify( lines, count );
}
}
this.pageInfo[ level ].start = new_start;
this.pageInfo[ level ].count = count;
this.pageInfo[ level ].lastShown = new_start + lines > count - 1;
// this._report('next');
return new_start;
}
Scroller.prototype._report = function( caller ) {
var stack = this.pageInfo.length;
log(' .' + mission.screenID + ', ' + caller + ', stack has ' + stack );
if( stack === 1 || true) {
// _showProps( obj, objName, newLine, show_deep, expand_arrays, show_type )
log( cd._showProps(this.pageInfo, 'stack', 1,2,1) );
}
}
Scroller.prototype.constructor = Scroller;
var scroller = new Scroller();
function scrollText( rtnobj, src, option, buttons, endOfPage ) {
var that = scrollText;
var pg_srclen = (that.pg_srclen = that.pg_srclen || {}); // cache srclen to detect if it changes
var lastPage, startLine = 0,
srclen = src.length,
choice_key, choice_value; // for buttons & blank line
let cacheKey = keyPrefix + option;
if( !pg_srclen.hasOwnProperty( cacheKey ) // haven't measured
|| pg_srclen[ cacheKey ] !== srclen ) { // or srclen changed, remeasure
measureText( option, src );
pg_srclen[ cacheKey ] = srclen;
}
var textInfo = textIndices[ keyPrefix ][ option ];
var totalLines = textInfo.length;
var lines = MAXSCREENLINES - buttons - 1; // -1 for blank line above button
startLine = scroller.next( totalLines, lines - 1, endOfPage );// -1 to repeat last line of previous page
lastPage = endOfPage || ( totalLines - startLine <= lines );
let topIsBlank = true;
do { // don't start page with a blank line
if( !textInfo || startLine >= textInfo.length )
break;
let sol = textInfo[ startLine ][ 0 ],
eol = textInfo[ startLine ][ 1 ];
for( let ti = sol; ti < eol; ti++ ) {
let chr = src[ ti ];
if( chr !== ' ' && chr !== '\t' ) {
topIsBlank = false;
break;
}
}
if( topIsBlank ) {
// scroll backwards so last line of previous page starts this page
if( totalLines - startLine > lines ) {
startLine--;
scroller.decr();
} else { // except when it's the last page (else loop forever!)
startLine++;
scroller.incr();
}
}
} while( topIsBlank && startLine < totalLines );
if( formatting ) {
formattedPage.length = 0;
formatEols( src, textInfo, startLine, lines );
rtnobj.message = formattedPage.join( '\n' );
} else {
let start = textInfo[ startLine ][ 0 ];
let lastln = startLine + lines - 1;
lastln = lastln > totalLines - 1 ? totalLines - 1 : lastln;
let end = textInfo[ lastln ][ 1 ];
rtnobj.message = src.slice( start, end );
}
if( startLine + lines < totalLines ) { // more to show after this page
choice_key = 'ZZ_more';
choice_value = scrollButton;
} else { // it's less than a full page or we're at the bottom
choice_key = 'ZZ_return';
expandKeys.stn_optns_continue_key = expandText( keyPrefix + pageLabels[ currentScreenIdx ] + '_title' );
choice_value = getChoice( expandText( 'stn_optns_option_continue', expandKeys ) );
}
rtnobj.choice_key = choice_key;
rtnobj.choice_value = choice_value;
return totalLines;
}
function measureWord( word ) {
var that = measureWord;
var cache = (that.cache = that.cache || {});
if( !cache.hasOwnProperty( word ) ) {
cache[ word ] = strFontLen( word );
}
return cache[ word ];
}
function measurePhrase( str, start, end ) { // measure all char.s but '\n's
// called by measureText & mkChoicePage (for status line)
var that = measurePhrase;
var phrase = (that.phrase = that.phrase || []);
phrase.length = 0;
if( start >= end )
return 0;
var dst = 0, src = start || 0, len = str.length;
if( !end || end > len ) {
end = len;
}
while( src < end ) {
let chr = str[ src ];
if( chr !== '\n' )
phrase[ dst++ ] = chr;
src++;
}
// for arrays, defaultFont.measureString assumes spaces between each element
return strFontLen( phrase ) - (phrase.length - 1) * SpaceLen;
}
var textIndices = {}; // cache ends of each line so only measure once
var formattedPage = [];
var wordSplitter = /[ \t\n]+/;
function measureText( option, str ) { // measure str from start, store index of line ends
// the way the core source reads, if a '\n' is encountered before a line is forced to wrap,
// all its spaces are preserved. But if it's long enough that it must be formatted, it gets
// tokenized (so all runs of space char.s are replaced with a single space char), up to the point
// where the remainder won't wrap, after which spaces are again preserved.
// see addLongText in GuiDisplayGen.m
var that = measureText;
var CRs = (that.CRs = that.CRs || []);
CRs.length = 0;
var lines = 0, eolCache, index,
strlen = str.length;
if( !textIndices.hasOwnProperty( keyPrefix ) ) {// create entry for oxp
textIndices[ keyPrefix ] = getObject();
}
let oxpCache = textIndices[ keyPrefix ];
if( !oxpCache.hasOwnProperty( option ) ) { // create entry for this block of text
oxpCache[ option ] = getArray();
} else { // we're re-measuring, toss previous entry
oxpCache[ option ].length = 0;
}
eolCache = oxpCache[ option ];
for( index = 0; index < strlen; index++ ) { // find all newline chars
let idx = str.indexOf( '\n', index );
if( idx < 0 ) break;
CRs.push( idx );
index = idx;
}
if( index < strlen ) { // append newline if necessary
CRs.push( strlen );
}
index = 0;
var end, chr, format, lineF, wordF, currCR,
tokenizing, lastCR = 0;
for( var ci = 0, len = CRs.length; ci < len; ci++ ) {
tokenizing = false;
end = currCR = CRs[ ci ];
chr = str[ end ];
while( end > lastCR && (chr === ' ' || chr === '\t' || chr === '\n') ) {
end--; // strip trailing spaces
chr = str[ end ];
}
if( end !== CRs[ ci ] ) {
end++; // always point at char after last
}
lineF = measurePhrase( str, index, end );
format = floor(lineF / MAXSCREENWIDTH) > 0; // # line needing formatting
tokenizing = format;
let wi = 0, words = str.slice( index, end ).split( wordSplitter ),
wlen = words.length;
while( index <= end ) { // <= vs < so we preserve imbedded \n char's
if( !format ) { // output as is
eolCache.push( [index, currCR, lineF, (tokenizing ? null : false)] );
lines++;
index = currCR + 1; // +1 to start of next line; currCR is a '\n' char
break;
} else { // tokenized output until one line full
lineF = 0; wordF = 0;
let startIndex = index, fullLine = null;
for( ; wi < wlen; wi++ ) {
let word = words[ wi ];
wordF = measureWord( word );
if( wordF > MAXSCREENWIDTH ) { // it happens, I did it my own self!
let wdIdx = str.indexOf( word ),
wdlen = word.length;
if( lineF > 0 ) {
eolCache.push( [startIndex, wdIdx, lineF, null] );
lines++;
lineF = 0;
}
eolCache.push( [wdIdx, wdIdx + wdlen, wordF, false] );
wdIdx += wdlen;
eolCache.push( [wdIdx, wdIdx, 0, 'stub'] );
lines += 2; // core just outputs it and it'll create a blank line
startIndex = wdIdx;
continue;
}
if( lineF + SpaceLen + wordF > MAXSCREENWIDTH ) { // word won't fit
fullLine = true;
break;
}
if( lineF === 0 ) {
lineF = wordF;
} else {
lineF += SpaceLen + wordF;
}
if( wi < wlen - 1 ) {
index = str.indexOf( words[ wi + 1 ], index + word.length );
} else { // no more words
index = currCR + 1; // skip the return, break from while loop
}
} // end for line
eolCache.push( [startIndex, index - 1, lineF, fullLine] );
lineF = measurePhrase( str, index, end );
} // endif !format
lines++;
format = floor(lineF / MAXSCREENWIDTH) > 0;
} // end while( index <= end ), ie. an input line
lastCR = currCR;
} // end for loop over all CRs
return lines;
}
function removeDups( list1, list2 ) { // remove dups so each member is unique; list2 bows to list1
if( !list1 )
return;
var list = list2 || list1;
var lstlen = list.length;
while( lstlen-- ) {
let item = list[ lstlen ];
let idx = list1.indexOf( item );
if( (!list2 && idx !== lstlen)
|| (list2 && idx >= 0) ) { // splice creates garbage, so remove it manually
let started = list.length;
for( let li = lstlen; li < started; li++ ) {
list[ li ] = list [ li + 1 ];
}
list.length = started - 1;
}
}
}
function compareNumbers( a, b ) { return a - b; } // sort function
const TRAILERS = ",.;:})?!";
function formatEols( str, strLines, start, lines ) {
var that = formatEols;
var fmtLn = (that.fmtLn = that.fmtLn || []);
var primeBreaks = (that.primeBreaks = that.primeBreaks || []); // indices of prime spaces for expansion
var pettyBreaks = (that.pettyBreaks = that.pettyBreaks || []); // indices of lesser spaces
var lnNum = 0, strLinesLen = strLines.length,
// strlen = str.length, //cagfmt
currLine = start,
cutoff = start + lines < strLinesLen ? start + lines : strLinesLen;
//log( so.name, 'formatEols, currLine = ' + currLine + ', cutoff = ' + cutoff + ', strLinesLen = ' + strLinesLen //cagfmt
// + ', lines = ' + lines + ', SpaceLen = ' + SpaceLen.toFixed(3) ); //cagfmt
//let debug = false, cag = ''; //cagfmt
var sol, eol, lineF, tokenized, fullStretch, lastChI, wdStart;
while( lnNum < lines && currLine < cutoff && currLine < strLinesLen ) {
primeBreaks.length = 0;
pettyBreaks.length = 0;
// each item in strLines has:
// index of 1st char, index of eol char, font length from 1st to eol char, and
// whether it was tokenized: false vs true, null, where null indicats a non-tokenized
// line generated by line breaks (not str input), the difference being that false
// leaves any leading whitespace while null strips it
[sol, eol, lineF, tokenized] = strLines[ currLine ];
if( tokenized === 'stub' ) { // it's a stub for a wordF > MAXSCREENWIDTH
currLine++; // to account for blank line generated when core
continue; // outputs an overlong text line
}
//debug = str.slice( sol, eol ).indexOf( '2013.03.31.' ) >= 0; //cagfmt
////debug = debug || str.slice( sol, eol ).indexOf( '(OXP Performance' ) >= 0; //cagfmt
//cag = ''; //cagfmt
//cag += 'sol = ' + sol + ', eol = ' + eol + ', lineF = ' + lineF.toFixed(3) + ', tokenized = ' + tokenized + '\n'; //cagfmt
//cag += '"' + str.slice(sol,eol).replace(/\n/g, 'cR').replace(/\t/g, 'tB') + '"\n'; //cagfmt
let spaces = 0, doubleSpc = 0,
slack = floor((MAXSCREENWIDTH - lineF) / SpaceLen);// in spaces
//cag += 'initial slack = ' + slack; //cagfmt
fullStretch = tokenized === true ? true :
tokenized === null ? lineF > MAXSCREENWIDTH * 0.8 : false;
if( tokenized === null && !fullStretch ) { // last line in a paragraph
slack = floor(slack * lineF / MAXSCREENWIDTH); // don't over-stretch short lines
}
lastChI = eol - 1; // eol always points at ' ', '\n' or = strllen
wdStart = -1;
//if( eol < strlen && str[eol] !== '\n' && str[eol] !== ' ' ) //cagfmt
// cag += '\neol is OFF, point @ "' + str[eol] + '", eol = ' + eol //cagfmt
// + '...' + str.slice(eol-20, eol).replace(/\n/g, 'cR') //cagfmt
// + '|' + str[eol] + '|' //cagfmt
// + str.slice(eol+1, eol+20).replace(/\n/g, 'cR') + '...' //cagfmt
// ; //cagfmt
while( str[ lastChI ] === ' ' || str[ lastChI ] === '\t' ) {
lastChI--; // index of last non-space char
}
//cag += ', lastChI = ' + lastChI + ', slack: ' + slack + '\n'; //cagfmt
fmtLn.length = 0;
for( let si = sol, fl = 0; si <= lastChI; si++ ) { // copy a line
let chr = str[ si ];
if( chr === ' ' || chr === '\t' ) {
if( tokenized !== false && fl === 0 ) // gobble leading whitespace
continue;
if( wdStart > 0 && fl - wdStart < 3 ) { // short word, cannot be 1st one
pettyBreaks.unshift( wdStart - 1 ); // space before word
pettyBreaks.push( fl ); // unshift as prefer space before if can't have both
}
wdStart = -1;
spaces++; // count breaks for doubling up on spaces
if( tokenized !== false ) {
fmtLn[ fl++ ] = ' ';
while( si + 1 < lastChI ) {
let nchr = str[ si + 1 ];
if( nchr !== ' ' && nchr !== '\t' )
break;
si++; // discard extra whitespace if tokenized
}
continue;
}
}
if( wdStart < 0 ) {
wdStart = fl;
}
fmtLn[ fl ] = chr;
if( tokenized === false ) { // leave line untouched
fl++;
continue;
}
let prev = si > sol ? str[ si - 1 ] : null;
let next = si < lastChI ? str[ si + 1 ] : null;
if( si + 1 < lastChI && TRAILERS.indexOf( chr ) >= 0 ) { // + 1 to avoid eol
if( next === ' ' || next === '\t' )
primeBreaks.unshift( fl + 1 ); // trailing punctuation
}
if( si - 1 > sol && (chr === '(' || chr === '{' )) { // - 1 to avoid sol
if( prev === ' ' || prev === '\t' )
primeBreaks.push( fl - 1 ); // leading punctuation
}
if( chr === "'" ) {
if( next === ' ' || next === '\t' ) { // closing quote
if( si + 1 < lastChI ) pettyBreaks.push( fl + 1 );
} else if( prev === ' ' || prev === '\t' ) {// opening quote
if( si - 1 > sol ) pettyBreaks.unshift( fl - 1 );
} // else it's an apostrophe, do nothing
}
if( chr === '\\' && next === '"' ) { // escaped quotes (? any others) %%
let next2 = si + 1 < lastChI ? str[ si + 2 ] : null;
if( next2 === ' ' || next2 === '\t' ) { // closing quote
if( si + 2 < lastChI ) pettyBreaks.push( fl + 2 );
} else if( prev === ' ' || prev === '\t' ) {// opening quote
if( si - 1 > sol ) pettyBreaks.unshift( fl - 1 );
}
}
if( chr === '%' && si + 1 <= lastChI ) { // oolite plist support '%' escaping for brackets []
let nextChr = str[ si + 1 ];
if( nextChr === ']' ) pettyBreaks.push( fl + 1 );
if( nextChr === '[' ) pettyBreaks.unshift( fl - 1 );
}
fl++;
}
//cag += '\nprimeBreaks: ' + primeBreaks.length + '\n'; //cagfmt
//let cagsln = fmtLn.join(''); //cagfmt
//for( let idx = 0, len = primeBreaks.length; idx < len; idx++ ) { //cagfmt
// let prime = primeBreaks[idx]; //cagfmt
// cag += ', ' + prime + ': "' + cagsln.slice(prime, prime + 10).replace(/\n/g, 'cR') + '"'; //cagfmt
// } //cagfmt
//cag += '\npettyBreaks: ' + pettyBreaks.length + '\n'; //cagfmt
//for( let idx = 0, len = pettyBreaks.length; idx < len; idx++ ) { //cagfmt
// let petty = pettyBreaks[idx]; //cagfmt
// cag += ', ' + petty + ': "' + cagsln.slice(petty, petty + 10).replace(/\n/g, 'cR') + '"'; //cagfmt
// } //cagfmt
//cag += '\n'; //cagfmt
if( tokenized !== false ) {
removeDups( primeBreaks );
removeDups( pettyBreaks );
removeDups( primeBreaks, pettyBreaks );
// _showProps( obj, objName, newLine, show_deep, expand_arrays, show_type )
//cag += cd._showProps( primeBreaks, 'primeBreaks.dups', 1, 1, 1 ) + ' len: ' + primeBreaks.length + '\n'; //cagfmt
//cag += cd._showProps( pettyBreaks, 'pettyBreaks.dups', 1, 1, 1 ) + ' len: ' + pettyBreaks.length + '\n'; //cagfmt
let len = primeBreaks.length; // primeBreaks get 1st priority
if( len > 0 ) {
if( slack >= len ) { // enough room for all
slack -= len;
} else {
primeBreaks.length = slack;
slack = 0;
}
}
len = pettyBreaks.length; // pettyBreaks get 2nd priority
if( len > 0 ) {
if( slack >= len ) { // enough room for all
slack -= len;
} else {
pettyBreaks.length = slack;
slack = 0;
}
}
len = pettyBreaks.length;
while( len-- ) { // concat arrays
let brk = pettyBreaks.pop();
if( primeBreaks.indexOf( brk ) < 0 ) { // no dup, else steal from doubleSpc
primeBreaks.push( brk );
}
}
primeBreaks.sort( compareNumbers );
//cag += cd._showProps( primeBreaks, 'primeBreaks.concat.sort', 1, 1, 1 ) //cagfmt
// + ' len: ' + primeBreaks.length + '\n'; //cagfmt
if( slack > 0 ) { // use remaining slack to double up spaces
doubleSpc = slack <= spaces || fullStretch // use all slack
? slack : spaces; // or double as many as slack allows
//cag += 'fullStretch = ' + fullStretch + ', doubleSpc = ' + doubleSpc + ', slack = ' + slack + ', spaces = ' + spaces + '\n'; //cagfmt
}
}
//let tmpF = strFontLen( fmtLn.join('') ); //cagfmt
//if( tmpF > MAXSCREENWIDTH ) //cagfmt
// cag += '\n,\t* fmtLn * over MAXSCREENWIDTH, tmpF = ' + tmpF.toFixed(3) + ': "' + fmtLn.join('') + '"'; //cagfmt
let primeBrk = primeBreaks.shift(),
line = '';
/// //////////////////////////////////////////////
//cag += ' (' + spaces + ' spaces), slack -> ' + slack + '\n'; //cagfmt
//if( primeBrk ) { //cagfmt
// cag += 'primeBrk = ' + primeBrk + ': "' + cagsln.slice(primeBrk, primeBrk + 10).replace(/\n/g, 'cR'); //cagfmt
// for( let idx = 0, len = primeBreaks.length; idx < len; idx++ ) { //cagfmt
// let lead = primeBreaks[idx]; //cagfmt
// cag += '", ' + lead + ': "' + cagsln.slice(lead, lead + 10).replace(/\n/g, 'cR'); //cagfmt
// } //cagfmt
// cag += '"'; //cagfmt
// } //cagfmt
//cag += '\ndoubleSpc = ' + doubleSpc + (fullStretch ? ', fullStretch = ' + fullStretch : '') + '\n'; //cagfmt
//cag += '\n'; //cagfmt
//if( debug ) log( so.name, cag ); //cagfmt
//cag = ''; //cagfmt
/// //////////////////////////////////////////////
for( let si = 0, flLen = fmtLn.length; si < flLen; si++ ) { // assemble new line
let chr = fmtLn[ si ];
//if( chr === ' ' || chr === '\t' ) //cagfmt
// cag += 'si = ' + (si<10?' ':'') + si + ', chr = "' + cagsln.slice(si, si + 10).replace(/\n/g, 'cR') + '"'; //cagfmt
if( chr === ' ' || chr === '\t' ) {
if( primeBrk && primeBrk === si ) {
if( lineF + SpaceLen > MAXSCREENWIDTH )
break; // safety valve
line += ' ';
lineF += SpaceLen;
primeBrk = primeBreaks.shift();
//cag += ', primeBrk = ' + (primeBrk ||''); //cagfmt
//cag += ', lineF = ' + lineF.toFixed(3); //cagfmt
}
if( fullStretch && doubleSpc > 0 ) {
let extras = floor(doubleSpc/spaces + 0.5);
doubleSpc -= extras;
spaces--;
//cag += ', adding extras = ' + extras + ', doubleSpc = ' + doubleSpc + '"'; //cagfmt
while( extras-- ) {
if( lineF + SpaceLen > MAXSCREENWIDTH )
break;// safety valve
line += ' ';
lineF += SpaceLen;
}
//cag += ', lineF = ' + lineF.toFixed(3); //cagfmt
} else if( doubleSpc > 0 ) {
if( lineF + SpaceLen > MAXSCREENWIDTH )
break; // safety valve
line += ' ';
lineF += SpaceLen;
doubleSpc--;
//cag += ', doubleSpc = ' + doubleSpc; //cagfmt
//cag += ', lineF = ' + lineF.toFixed(3); //cagfmt
}
}
line += fmtLn[ si ];
//if( chr === ' ' || chr === '\t' ) cag += '\n'; //cagfmt
}
formattedPage.push( line );
currLine++;
lnNum++;
//tmpF = strFontLen( line ); //cagfmt
//cag += '\n.\t"' + line.replace(/\n/g, 'cR') + '" ' + tmpF.toFixed(3) + '\n'; //cagfmt
//cag += '.\t 0....5....1....5....2....5....3....5....4....5....$....5....6....5....7....5....8....5....9....5....0\n'; //cagfmt
//cag += '.\t"' + fmtLn.join('').replace(/\n/g, 'cR') + '"\n\n'; //cagfmt
//if( tmpF > MAXSCREENWIDTH ) //cagfmt
// cag += 'line over MAXSCREENWIDTH, tmpF = ' + tmpF.toFixed(3) + ': "' + line + '"\n\n'; //cagfmt
//if( debug ) log( so.name, cag ); //cagfmt
}
return currLine < strLinesLen - 1; // is there more, ie. do we scroll
}
///////////////////////////////////////////////////////////////////////////////
// functions for oxp variables ////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// : ' + + '
function stripPrefix( term ) {
if( startsWith( term, hostVarPrefix ) ) {
return term.slice( hostVarPrefix.length );
}
return term;
}
function isSingleQuoted( term ) {
return startsWith( term, "'" ) && endsWith( term, "'" );
}
function stripQuotes( term ) {
if( isSingleQuoted( term ) ) {
return term.slice( 1, term.length - 2 );
} else if( startsWith( term, '"' ) && endsWith( term, '"' ) ) {
///should never happen
log( 'stripQuotes, * * * how did term become double quoted: ' + term );
term = term.slice( 1, term.length - 2 );
return term.replace( '"', "'" );
}
return term;
}
function confirmReset() {
var that = confirmReset;
var choices = (that.choices = that.choices || {'01_yes': 'Yes', '02_no': 'No'});
var screen = (that.screen = that.screen || {});
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // static
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.screenID = 'stn_optns_confirmReset_' + keyPrefix;
screen.allowInterrupt = false;
screen.textEntry = false;
screen.choices = choices;
screen.initialChoicesKey = '02_no';
}
var currentScreen = pageLabels[ currentScreenIdx ],
page_key = keyPrefix + currentScreen + '_title';
expandKeys.stn_optns_reset_page = expandText( page_key );
screen.title = expandText( 'stn_optns_reset_title', expandKeys );
screen.message = expandText( 'stn_optns_reset_summary', expandKeys );
mission.runScreen( screen, performReset.bind( so ) );
}
function performReset( choice ) {
try {
if( choice === '01_yes' ) {
let currentScreen = pageLabels[ currentScreenIdx ],
options = optionPages[ currentScreen ];
for( let idx = 0, len = options.length; idx < len; idx++ ) {
resetOptionValue( options[ idx ] );
}
}
displayOptions( 'XX_reset' );
} catch( err ) {
log( so.name, so._reportError( err, performReset, choice ) );
throw err;
}
}
function equalValue( a, b ) {
if( Array.isArray( a ) ) {
if( !Array.isArray( b ) )
return false;
if( a.length !== b.length )
return false;
for( let idx = 0, len = a.length; idx < len; idx++ ) {
if( a[ idx ] !== b[ idx ] )
return false;
}
return true;
}
if( typeof a === 'object' ) {
if( typeof b !== 'object' ) return false;
let asprops = Object.getOwnPropertyNames( a );
let bsprops = Object.getOwnPropertyNames( b );
if( asprops.length !== bsprops.length ) return false;
for( let idx = 0, len = asprops.length; idx < len; idx++ ) {
if( asprops.indexOf( bsprops[ idx ] ) < 0 )
return false;
}
asprops = freeArray( asprops );
bsprops = freeArray( bsprops );
for( let x in a ) {
if( a.hasOwnProperty( x ) && b.hasOwnProperty( x )
&& a[ x ] === b[ x ] ) {
continue;
} else {
return false;
}
}
return true;
}
return a === b;
}
function getHostValue( option ) {
if( !hostOxp.hasOwnProperty( hostVarPrefix + option ) )
return; // undefined
return hostOxp[ hostVarPrefix + option ];
}
function setHostVar( option, value ) {
hostOxp[ hostVarPrefix + option ] = copyValue( value );
}
function getOptionValue( option, fromHost ) { // fromHost => try getting value from host first
if( !fromHost ) {
if( changesMade.hasOwnProperty( option ) ) {
return copyValue( changesMade[ option ] );
}
}
let hostVar = getHostValue( option ); // change not pending, return curr. value
return copyValue( hostVar );
}
function setOptionValue( option, value ) {
function setCurrently( option, value ) {
freeValue( info.currently )
info.currently = copyValue( value );
}
var that = setOptionValue;
var soKeys = (that.soKeys = that.soKeys || {});
clearObject( soKeys );
var info = optionInfo[ option ];
var hostVal = getHostValue( option ),
newVal = changesMade[ option ];
if( changesMade.hasOwnProperty( option ) ) { // already been changed
if( equalValue( value, newVal ) ) { // same as existing change, nothing to do
return;
} else if( equalValue( value, hostVal ) ) { // changed back to original value
setCurrently( option, value );
removeChangeMade( option );
doAssignments( option );
doExecutes( option );
return;
}
} else if( equalValue( value, hostVal ) ) { // entered original value
return;
}
setCurrently( option, value )
if( changesMade.hasOwnProperty( 'cag$stnOptsPending' ) ) {
changesMade[ 'cag$stnOptsPending' ] += 1;
} else {
changesMade[ 'cag$stnOptsPending' ] = 1;
}
missionKeys.stn_optns_changes_count = changesMade[ 'cag$stnOptsPending' ];
if( missionKeys.stn_optns_changed.hasOwnProperty( option ) ) {
freeValue( missionKeys.stn_optns_changed[ option ] );
}
if( Array.isArray( value ) ) { // don't reference originals, use copy
changesMade[ option ] = getArray( value );
missionKeys.stn_optns_changed[ option ] = getArray( value );
if( missionKeys.hasOwnProperty( option ) ) {
missionKeys[ option ] = getArray( value );
}
} else if( typeof value === 'object' ) {
changesMade[ option ] = getObject( value );
missionKeys.stn_optns_changed[ option ] = getObject( value );
if( missionKeys.hasOwnProperty( option ) ) {
missionKeys[ option ] = getObject( value );
}
} else {
changesMade[ option ] = value;
missionKeys.stn_optns_changed[ option ] = value;
if( missionKeys.hasOwnProperty( option ) ) {
missionKeys[ option ] = value;
}
}
doAssignments( option );
doExecutes( option );
}
function resetOptionValue( option ) {
var info = optionInfo[ option ];
if( info.type === 'display' )
return; // has no variable associated with option
if( info.option && info.defawlt !== undefined ) {
setOptionValue( option, info.defawlt );
}
}
function removeChangeMade( option ) {
var that = removeChangeMade;
var soKeys = (that.soKeys = that.soKeys || {});
clearObject( soKeys );
if( changesMade.hasOwnProperty( option ) ) {
freeValue( changesMade[ option ] );
delete changesMade[ option ];
if( changesMade[ 'cag$stnOptsPending' ] <= 1 ) {
delete changesMade[ 'cag$stnOptsPending' ];
missionKeys.stn_optns_changes_count = 0;
} else {
changesMade[ 'cag$stnOptsPending' ] -= 1;
missionKeys.stn_optns_changes_count -= 1;
}
}
if( missionKeys.stn_optns_changed.hasOwnProperty( option ) ) {
freeValue( missionKeys.stn_optns_changed[ option ] );
delete missionKeys.stn_optns_changed[ option ];
}
}
// : ' + + '
var optionsChanged = [];
var pagesChanged = [];
var changesDetail = [];
function saveChoices() { // commit changes to host oxp
optionsChanged.length = 0; // re-use arrays
pagesChanged.length = 0;
changesDetail.length = 0;
var rpt = 'none', longestF = 0;
var rmdr = getRemindObj();
if( changesMade.hasOwnProperty( 'cag$stnOptsPending' ) ) {
for( let option in changesMade ) {
if( changesMade.hasOwnProperty( option ) ) {
if( option === 'cag$stnOptsPending' )
continue;
// if( !hostOxp.hasOwnProperty( hostVarPrefix + option ) ) continue;
// - no, this can be used to init non-existent properties
let hostVal = getOptionValue( option, true );
let newVal = changesMade[ option ];
setHostVar( option, newVal )
if( typeof newVal === 'object' ) {
// remove only reference so object will persist in hostOxp
// - later call to clearObject() would free any arrays/objects
delete changesMade[ option ];
}
if( rmdr.reportSummary ) {
let lenF = measureWord( option );
if( lenF > longestF ) {
longestF = lenF;
}
changesDetail.push( [option, hostVal, newVal] );
} else {
freeValue( hostVal );
}
optionsChanged.push( option ); // save variable name
let page;
if( optionInfo.hasOwnProperty( option ) ) {
page = optionInfo[ option ].page;
} else {
page = 'unknown!';
}
if( pagesChanged.indexOf( page ) < 0 )
pagesChanged.push( page ); // one of two arrays sent via notifyCallback
}
}
}
if( notifyCallback ) // inform hostOxp what's been altered
notifyCallback( optionsChanged, pagesChanged );
if( callPWSG ) // write to missionVariables; user still req'd to save game
hostOxp.playerWillSaveGame();
if( rmdr.reportSummary ) {
rpt = '';
longestF += SpaceLen * 4; // add small indent for list (don't touch window's edge)
let spacing = paddingText( '', SpaceLen * 2 ),
spacingLen = SpaceLen * 2; // spacing between column of options & values
for( let ci = 0, clen = changesDetail.length; ci < clen; ci++ ) {
let [option, hostVal, newVal] = changesDetail[ ci ];
let heading = paddingText( option, longestF ) + option + COLON + spacing;
let prev = rptChange( option, hostVal ),
curr = rptChange( option, newVal ),
info = optionInfo[ option ];
if( info.type === 'bitflags' && prev === '' && curr === '' ) {
// rptChange will return the value if it cannot find the bitflagStrs
rpt += rptBitFlags( heading, longestF + measureWord( COLON ) + spacingLen );
// these add space between option & flags
} else {
rpt += wrapRight( heading + prev + ARROW + curr, -1, longestF, longestF );
}
freeValue( hostVal );
}
reportSavegame( rpt );
} else if( rmdr.remindAutosave ) {
reportSavegame( 'no_summary' );
} else {
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references)
}
}
///////////////////////////////////////////////////////////////////////////////
// functions for summary page /////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
var fromCol = [], toCol = []; // for formatting bit flag summary
function wrapRight( str, strLen, indent, shortenFirst ) {
shortenFirst = shortenFirst || 0;
if( !strLen || strLen <= 0 ) { // caller doesn't have length on hand
strLen = measureWord( str );
}
var pad = indent <= shortenFirst ? 0 : indent - shortenFirst;
if( indent - shortenFirst + strLen < MAXSCREENWIDTH ) { // fits on one line
return (pad ? paddingText( '', pad ) : '') + str + '\n';
}
var line = '', total = 0, wrapped = '', words = str.split( COMMA );
while( words.length > 0 ) {
let nextWord = words.shift();
let nextWordLen = measureWord( nextWord );
let commaLen = measureWord( COMMA );
if( total === 0 || total + commaLen + nextWordLen > MAXSCREENWIDTH ) {
// first line (may have a shortener) OR line is full
if( total > 0 ) {
wrapped += line + '\n';
line = '';
pad = indent;
total = 0;
}
if( pad + nextWordLen > MAXSCREENWIDTH ) {// don't go past edge
pad = MAXSCREENWIDTH - nextWordLen;
}
line += (pad > 0 ? paddingText( '', pad ) : '') + nextWord;
total += pad + nextWordLen;
} else {
line += COMMA + nextWord;
total += commaLen + nextWordLen;
}
}
if( line )
wrapped += line + '\n';
return wrapped;
}
function rptBitFlags( label, labelLen ) {
/*
OptionName: old_flag, second_flag -> new_flag, new_second
OR, if option & from fit
OptionName: old_flag, second_flag
-> new_flag, new_second
*/
var fromFmt = '', fromLen = 0, toFmt = '', toLen = 0;
var commaLen = measureWord( COMMA ),
arrowLen = measureWord( ARROW );
if( fromCol.length ) {
// not measuring final formatted set because measureWord caches
for( let ci = 0, len = fromCol.length; ci < len; ci++ ) {
fromFmt += fromCol[ ci ];
fromLen += measureWord( fromCol[ ci ] );
if( len > 1 && ci < len - 1 ) {
fromFmt += COMMA;
fromLen += commaLen;
}
}
} else {
fromFmt = '<none>';
fromLen = measureWord( fromFmt );
}
if( toCol.length ) {
for( let ci = 0, len = toCol.length; ci < len; ci++ ) {
toFmt += toCol[ ci ];
toLen += measureWord( toCol[ ci ] );
if( len > 1 && ci < len - 1 ) {
toFmt += COMMA;
toLen += commaLen;
}
}
} else {
toFmt = '<none>';
toLen = measureWord( toFmt );
}
var rpt;
if( labelLen + fromLen + arrowLen + toLen <= MAXSCREENWIDTH ) {
// option and both sets of flags fit in one line
rpt = label + fromFmt + ARROW + toFmt + '\n';
} else {
rpt = wrapRight( label + fromFmt, labelLen + fromLen, labelLen, labelLen );
let indentLen = SpaceLen * 2; // shift 'to' flags so not inline w/ 'from'
rpt += wrapRight( ARROW + toFmt, arrowLen + toLen, indentLen + labelLen, arrowLen );
}
fromCol.length = 0; // reset for next rpt
toCol.length = 0;
return rpt;
}
function noParens( str ) {
let spacePending = false, inParen = false, parsed = '';
for(let x = 0, len = str.length; x < len; x++) {
let ch = str[ x ];
if( ch === '(' ) {
inParen = true;
} else if( ch === ')' ) {
inParen = false;
spacePending = true;
} else if( ch === ' ' ) {
spacePending = true;
} else if( !inParen ) {
if( !spacePending ) {
parsed += ch;
} else if( PUNCTUATION.indexOf( ch ) >= 0 ) {
parsed += ch;
spacePending = false;
} else if( ch !== ' ' ){
parsed += ' ' + ch;
spacePending = false;
}
}
}
return parsed;
}
function rptChange( option, value ) {
var info = optionInfo[ option ];
var rpt = '', type = info.type;
if( type === 'text' ) {
rpt = "'" + value + "'";
} else if( type === 'toggle' ) {
rpt = expandText( ( value ? 'stn_optns_boolean_true' : 'stn_optns_boolean_false' ) );
} else if( type === 'vector' ) {
rpt = "[ " + value.join( ', ' ) + " ]" ;
} else if( type === 'choice' ) {
rpt = info.selection[ value ];
} else if( type === 'bitflags' ) {
let flags = info.selection;
if( !flags ) {
rpt = value;
} else {
let column = fromCol.length === 0 ? fromCol : toCol;
if( flags && flags.length > 0 ) {
for( let ci = 0, len = flags.length; ci < len; ci++ ) {
let flag = 1 << ci;
if( value & flag ) {
// flag is set, prep columns for rptBitFlags
column.push( noParens( flags[ ci ] ) );
}
}
}
}
} else {
rpt = value;
}
return rpt;
}
var NAG_COUNT = 3;
var remindScreenMsg = '';
function reportSavegame( rpt, choice ) {
var that = reportSavegame;
var choices = (that.choices = that.choices || {});
var notChoices = (that.notChoices = that.notChoices || {});
var screen = (that.screen = that.screen || {});
var remind = (that.remind = that.remind || {});
if( !screen.hasOwnProperty( 'exitScreen' ) ) { // first time ever, create statics
screen.exitScreen = 'GUI_SCREEN_INTERFACES';
screen.screenID = 'stn_optns_reportSavegame_' + keyPrefix;
screen.allowInterrupt = false;
screen.textEntry = false;
choices[ '01_OK' ] = getChoice( expandText( 'stn_optns_remind_savegame_acknowledge' ) );
choices[ '02_NONAG' ] = getChoice( expandText( 'stn_optns_remind_savegame_quit_nagging' ) );
choices[ '03_NORPT' ] = getChoice( expandText( 'stn_optns_remind_savegame_quit_reporting' ) );
screen.initialChoicesKey = '01_OK';
screen.choices = choices;
}
var rmdr = getRemindObj();
let defaultColor = expandText( 'stn_optns_default_color' ) || 'yellowColor';
var returnToTop = false;
if( rpt ) { // first time through this summary page
remind.reportSummary = rmdr.reportSummary; // preserve state
remind.remindAutosave = rmdr.remindAutosave;
remind.autosaveStopRemind = rmdr.autosaveStopRemind;
if( choices.hasOwnProperty( 'ZZ_more' ) ) { // don't know if it'll be needed
delete choices[ 'ZZ_more' ]; // don't freeChoice as it's a global button
}
for( let x in notChoices ) { // restore dynamic buttons
if( notChoices.hasOwnProperty( x ) ) { // move into choices
choices[ x ] = notChoices[ x ];
delete notChoices[ x ];
}
}
for( let x in choices ) { // restore button color
if( choices.hasOwnProperty( x ) ) {
if( x === '02_NONAG' ) {
choices[ x ].color = defaultColor;
if( !rmdr.remindAutosave ) { // store it for future use
notChoices[ x ] = choices[ x ];
delete choices[ x ];
}
} else if( x === '03_NORPT' ) { // summary button remains as an 'undo' until nagging is done
if( rmdr.reportSummary ) {
choices[ x ].text = expandText( 'stn_optns_remind_savegame_quit_reporting' );
} else {
choices[ x ].text = expandText( 'stn_optns_remind_savegame_start_reporting' );
choices[ x ].color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
}
} else {
choices[ x ].color = defaultColor;
}
}
}
} else { // process reply
if( choice === 'ZZ_more' ) { // preserve static more button
if( choices[ choice ] === scrollTopButton ) {
returnToTop = true;
}
choices[ choice ] = scrollButton;
} else if( choice === '02_NONAG' ) {
let prev = remind.autosaveStopRemind,
curr = rmdr.autosaveStopRemind;
if( prev === false || (prev === NAG_COUNT - 1 && curr >= NAG_COUNT) ) {
// disabled by hostOxp using suppressSummary OR player exceeding NAG_COUNT
rmdr.remindAutosave = false; // disable the nagging
notChoices[ choice ] = choices[ choice ];// move into notChoices
delete choices[ choice ];
} else { // choice remains as a toggle
if( abs(curr - prev) > 1 ) { // only one decrement per summary page; 2nd one undoes 1st
rmdr.autosaveStopRemind = prev;
}
if( abs(curr - prev) % 2 > 0 ) {
choices[ choice ].color = expandText( 'stn_optns_optionChanged_color' ) || 'cyanColor';
}
}
} else if( choice === '03_NORPT' ) { // choice remains as a toggle
let prev = remind.reportSummary,
curr = rmdr.reportSummary;
if( prev && curr ) { // was toggled back on
choices[ choice ].color = defaultColor;
} else if( prev && !curr ) { // was toggled off
choices[ choice ].color = expandText( 'stn_optns_optionChanged_color' ) || 'cyanColor';
} else if( !prev && curr ) { // was restarted
choices[ choice ].color = expandText( 'stn_optns_optionChanged_color' ) || 'cyanColor';
} else { // !prev && !curr // restart was undone
choices[ choice ].color = expandText( 'stn_optns_notInUse_color' ) || 'darkGrayColor';
}
}
}
var willReport = remind.reportSummary && rpt !== 'no_summary' && rpt !== 'none',
willRemind = remind.remindAutosave; // rpt is null when we return to process buttons
if( willReport ) {
screen.title = expandText( 'stn_optns_remind_summary_title' );
} else if( willRemind ) {
screen.title = expandText( 'stn_optns_remind_savegame_title' );
} else { // nothing to do
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references)
return;
}
if( rpt ) { // first time through this summary page
let count = changesMade[ 'cag$stnOptsPending' ];
remindScreenMsg = willReport ? expandText( 'stn_optns_remind_list_header' ) + rpt : '';
expandKeys.stn_optns_changes_count = count;
expandKeys.stn_optns_changes_plurality = expandText( ( count > 1 ? 'stn_optns_remind_is_plural'
: 'stn_optns_remind_is_single' ) );
expandKeys.stn_optns_changes_verb = expandText( ( count > 1 ? 'stn_optns_remind_plural_verb'
: 'stn_optns_remind_single_verb' ) );
expandKeys.stn_optns_changes_pronoun = expandText( ( count > 1 ? 'stn_optns_remind_plural_pronoun'
: 'stn_optns_remind_single_pronoun' ) );
if( willRemind && willReport ) {
remindScreenMsg += expandText( 'stn_optns_remind_savegame_summary_autosave', expandKeys );
} else if( willReport ) {
remindScreenMsg += expandText( 'stn_optns_remind_savegame_summary', expandKeys );
} else { // willRemind, as the !willReport && !willRemind case returns above
remindScreenMsg += expandText( 'stn_optns_remind_savegame_autosave'
+ remind.autosaveStopRemind, expandKeys );
}
scroller.start();
} // else we're scrolling or toggling buttons
if( returnToTop ) {
scroller.reset();
}
scrollText( rtnobj, remindScreenMsg, 'reportSavegame',
Object.keys( choices ).length + 1 ); // +1 for blank line
let choice_key = rtnobj.choice_key;
choices[ choice_key ] = rtnobj.choice_value;
if( choice_key === 'ZZ_more' && !choices.hasOwnProperty( 'ZZ_more' ) ) {
choices[ 'ZZ_more' ] = scrollButton;
} else if( choice_key === 'ZZ_return' && choices.hasOwnProperty( 'ZZ_more' ) ) {
// repurpose for returning to top of list
choices[ 'ZZ_more' ] = scrollTopButton;
}
screen.initialChoicesKey = choice_key === 'ZZ_more' ? choice_key : '01_OK';
screen.message = rtnobj.message;
mission.runScreen( screen, remindAcknowledged.bind( so ) );
}
function getRemindObj( prefix ) { // may be called 'locally' (prefix is undefined) or for a
// specific hostOxp (prefix is its keyPrefix) - see _getReminder4Oxp
var rmdr = so.$Remind2Savegame;
var forOxp = prefix || keyPrefix;
if( !rmdr || !rmdr.hasOwnProperty( forOxp ) ) { // may have replies to other hostOxp's
let oldStyle = null;
if( !rmdr ) {
// re-check missionVariables in case $Remind2Savegame was clobbered by different hostOxp
rmdr = JSON.parse( missionVariables.$StationOptionsRemind2Savegame );
if( rmdr === null || typeof rmdr === 'number' ) { // not present OR old format
if( typeof rmdr === 'number' ) {
oldStyle = rmdr;
}
rmdr = getObject();
}
so.$Remind2Savegame = rmdr;
}
rmdr[ forOxp ] = getObject(); // init
rmdr[ forOxp ].reportSummary = true;
rmdr[ forOxp ].remindAutosave = true;
rmdr[ forOxp ].autosaveStopRemind = oldStyle === null ? 0 : oldStyle; // could be zero
rmdr[ forOxp ].suppressSummary = false;
}
let curr = rmdr[ forOxp ];
if( !prefix ) { // called 'locally', otherwise suppressSummary is undefined
// or for a (possibly) different oxp
// check here (vs init) to be dynamic (detect when player changes, allow hostOxp freedom to act)
if( curr.suppressSummary !== suppressSummary ) {
curr.reportSummary = !suppressSummary || suppressSummary !== 'summary';
curr.remindAutosave = (!suppressSummary || suppressSummary !== 'autosave')
&& curr.autosaveStopRemind < NAG_COUNT;
curr.suppressSummary = suppressSummary;
}
}
if( gameSettings.autosave )
curr.remindAutosave = false;
return curr;
}
function remindAcknowledged( choice ) {
try {
if( choice === '02_NONAG' ) {
getRemindObj().autosaveStopRemind++;
reportSavegame( null, '02_NONAG' );
} else if( choice === '03_NORPT' ) {
let rmdr = getRemindObj();
rmdr.reportSummary = !rmdr.reportSummary;
reportSavegame( null, '03_NORPT' );
} else if( choice === 'ZZ_more' ) {
reportSavegame( null, 'ZZ_more' );
} else if( choice === 'ZZ_return' ) {
scroller.stop();
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references)
displayOptions( 'WW_next_pg' );
} else { // choice === '01_OK'
remindScreenMsg = '';
clearObject( changesMade, true ); // true => keepObjects (ie. del prop only, not what it references)
}
} catch( err ) {
log( so.name, so._reportError( err, remindAcknowledged, choice ) );
throw err;
}
}
///////////////////////////////////////////////////////////////////////////////
// utility functions //////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
function freeValue( value ) {
if( Array.isArray( value ) ) {
freeArray( value );
} else if( typeof value === 'object' ) {
freeObject( value );
}
return null; // convenience return
}
function copyValue( value ) {
if( value === null ) { // null is an object!
return value;
} else if( Array.isArray( value ) ) {
return getArray( value ); // don't ref originals, use copy
} else if( typeof value === 'object' ) {
return getObject( value ); // don't ref originals, use copy
}
return value;
}
var object_pool = [];
function clearObject( obj, keepObjects ) {
for( var x in obj ) { // re-use object
if( obj.hasOwnProperty( x ) ) {
if( !keepObjects ) {
freeValue( obj[ x ] );
}
// not setting to null as hasOwnProperty is frequently relied upon
delete obj[ x ];
}
}
return null; // convenience return
}
function freeObject( obj, keepObjects ) {
if( !obj || typeof obj !== 'object' )
return;
clearObject( obj, keepObjects ); // scrub old data
object_pool.push( obj ); // toss into recycle bin
return null; // convenience return
}
function getObject( elements ) {
var obj = object_pool.length > 0 ? object_pool.pop() : {};
if( elements && typeof elements === 'object' ) {
for( var x in elements ) {
if( elements.hasOwnProperty( x ) ) {
obj[ x ] = elements[ x ];
}
}
}
return obj;
}
var array_pool = [];
function freeArray( array ) {
if( !array || !Array.isArray( array ) )
return;
for( var idx = 0, len = array.length; idx < len; idx++ ) {
freeValue( array[ idx ] );
}
array.length = 0; // scrub old data
array_pool.push( array ); // toss into recycle bin
return null; // convenience return
}
// : ' + + '
function getArray( elements ) {
var array = array_pool.length > 0 ? array_pool.pop() : [];
// as we're in strict mode, we cannot access 'caller', 'callee', and 'arguments' properties
// but we can make a copy? whatever
var args = Array.prototype.slice.call( arguments );
var aCount = args.length;
if( aCount > 1 ) {
while( aCount-- ) {
array[ aCount ] = args[ aCount ];
}
} else if( elements && Array.isArray( elements ) ) {
let x = elements.length;
while( x-- ) {
array[ x ] = elements[ x ];
}
}
return array;
}
var choice_pool = [];
function getChoice( text ) {
var choice = choice_pool.length > 0 ? choice_pool.pop() : {};
choice.text = text;
choice.color = expandText( 'stn_optns_default_color' ) ||'yellowColor';
choice.alignment = 'CENTER';
choice.unselectable = false;
return choice;
}
function freeChoice( choice ) {
if( !choice || typeof choice !== 'object' )
return;
clearObject( choice );
choice_pool.push( choice );
}
function poolReport( msg ) {
log('poolReport, ' + msg );
log('poolReport, choice_pool: ' + choice_pool.length + ', array_pool: ' + array_pool.length
+ ', object_pool: ' + object_pool.length );
}
function _purgepools( report ) { // called upon page change & exit, free up any abandoned changes & all memory pools
if( report ) {
poolReport( (typeof report === 'string' ? report : '_purgepools') );
}
choice_pool.length = 0; // empty pools for garbage collection
array_pool.length = 0;
object_pool.length = 0;
}
/*
function chrCount( ch, str ) { // return number of 'ch' characters in 'str'
// fastest of 5, no garbage, 2x faster than as a String.prototype
for( var count = -1, index = -2; index !== -1;
count++, index = str.indexOf( ch, index + 1 ) );
return count;
}
*/
/*
this is duplicated from my revised version of _paddingText in
..\Resouces\Scripts\oolite-contracts-helpers.js
- remove (and update ref's) when/if it's incorporated into the core
*/
function paddingText( currentText, desiredLength ) {
var that = paddingText;
var strFontLen = (that.strFontLen = that.strFontLen || defaultFont.measureString);
var hairSpace = (that.hairSpace = that.hairSpace || String.fromCharCode( 0x200a ));
// 1/8 width space; while there are also 1/4 & 1/2 width unicode chars, MacOS only supports this one!
var hairSpaceLength = (that.hairSpaceLength = that.hairSpaceLength || strFontLen( hairSpace ));
var spaceLength = (that.spaceLength = that.spaceLength || strFontLen( ' ' ));
var padding = (that.padding = that.padding || []);
if( desiredLength <= 0 ) return '';
var num, padString = '';
var currentLength = strFontLen( currentText );
var lengthNeeded = desiredLength - currentLength;
if( lengthNeeded <= 0 ) return '';
num = floor( lengthNeeded / spaceLength ) + 1;
if( num > 1 ) {
padding.length = num;
padString += padding.join( ' ' );
}
lengthNeeded -= num <= 0 ? 0 : ( num - 1 ) * spaceLength;
num = floor( lengthNeeded / hairSpaceLength ) + 1;
if( num > 1 ) {
padding.length = num;
padString += padding.join( hairSpace );
}
return padString;
}
return { _setInterfaces: _setInterfaces,
_registerHostOxp: _registerHostOxp,
_purgepools: _purgepools,
_getReminder4Oxp: _getReminder4Oxp,
_getAllowedCallback: _getAllowedCallback,
_getCallPWSG: _getCallPWSG,
_getNotifyCallback: _getNotifyCallback,
_getSuppressSummary: _getSuppressSummary,
_getMissionKeys: _getMissionKeys,
_setAllowedCallback: _setAllowedCallback,
_setCallPWSG: _setCallPWSG,
_setNotifyCallback: _setNotifyCallback,
_setSuppressSummary: _setSuppressSummary,
_setMissionKeys: _setMissionKeys,
_updateMissionKeys: _updateMissionKeys,
_resetLocalVars: _resetLocalVars,
};
// }; // end of closure
}.bind(this); // get [native code] in debugger rather than entire function
}).call(this);
// }).call(worldScripts.station_options);
/* (function() { // run BEFORE reloading entire script
var so = worldScripts.station_options;
for( let x in so ) {
if( so.hasOwnProperty( x ) )
delete so[ x ];
}
})() // */
/* (function() { // run AFTER reloading entire script
var so = worldScripts.station_options;
so.startUp();
var ws = worldScripts.telescope;
so._initStationOptions( ws, 'telescope_', ws._stnOptionsAllowed, true, ws._reloadFromStn );
so._setStationInterfaceEntries();
})() // */
|