1 /**
  2  * The Render Engine
  3  *
  4  * An extension to the engine for script loading and processing.
  5  *
  6  * @author: Brett Fattori (brettf@renderengine.com)
  7  *
  8  * @author: $Author$
  9  * @version: $Revision$
 10  *
 11  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 12  *
 13  * Permission is hereby granted, free of charge, to any person obtaining a copy
 14  * of this software and associated documentation files (the "Software"), to deal
 15  * in the Software without restriction, including without limitation the rights
 16  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 17  * copies of the Software, and to permit persons to whom the Software is
 18  * furnished to do so, subject to the following conditions:
 19  *
 20  * The above copyright notice and this permission notice shall be included in
 21  * all copies or substantial portions of the Software.
 22  *
 23  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 29  * THE SOFTWARE.
 30  *
 31  */
 32 
 33 //====================================================================================================
 34 //====================================================================================================
 35 //                                     SCRIPT PROCESSING
 36 //====================================================================================================
 37 //====================================================================================================
 38 /**
 39  * @class A static class which is used to load new JavaScript into the browser.  Methods are
 40  *          also provided to use AJAX to get text and JSON data on-the-fly, load stylesheets,
 41  *          and process script callbacks from a loader queue.
 42  * @static
 43  */
 44 R.engine.Script = Base.extend(/** @scope R.engine.Script.prototype */{
 45 
 46     constructor:null,
 47 
 48     /*
 49      * Script queue
 50      */
 51     scriptQueue:[],
 52     loadedScripts:{}, // Cache of loaded scripts
 53     scriptLoadCount:0, // Number of queued scripts to load
 54     scriptsProcessed:0, // Number of scripts processed
 55     scriptRatio:0, // Ratio between processed/queued
 56     queuePaused:false, // Script queue paused flag
 57     pauseReps:0, // Queue run repetitions while paused
 58     gameOptionsLoaded:false, // Whether the game options have loaded yet
 59     gameOptionsObject:{}, // Options object for the game
 60     uniqueRequest:true,
 61     callbacks:{}, // Script callbacks
 62 
 63     /**
 64      * Status message when a script is not found
 65      * @memberOf R.engine.Script
 66      * @type {Boolean}
 67      */
 68     SCRIPT_NOT_FOUND:false,
 69 
 70     /**
 71      * Status message when a script is successfully loaded
 72      * @memberOf R.engine.Script
 73      * @type {Boolean}
 74      */
 75     SCRIPT_LOADED:true,
 76 
 77     /**
 78      * Include a script file.
 79      *
 80      * @param scriptURL {String} The URL of the script file
 81      * @memberOf R.engine.Script
 82      */
 83     include:function (scriptURL) {
 84         R.engine.Script.loadNow(scriptURL);
 85     },
 86 
 87     /**
 88      * Perform an immediate load on the specified script.  Objects within
 89      * the script may not immediately initialize, unless their dependencies
 90      * have been resolved.
 91      *
 92      * @param {String} scriptPath The path to the script to load
 93      * @param {Function} [cb] The function to call when the script is loaded.
 94      *                   the path of the script loaded and a status message
 95      *                   will be passed as the two parameters.
 96      * @memberOf R.engine.Script
 97      * @private
 98      */
 99     loadNow:function (scriptPath, cb) {
100         R.engine.Script.doLoad(R.Engine.getEnginePath() + scriptPath, scriptPath, cb);
101     },
102 
103     /**
104      * Queue a script to load from the server and append it to
105      * the head element of the browser.  Script names are
106      * cached so they will not be loaded again.  Each script in the
107      * queue is processed synchronously.
108      *
109      * @param scriptPath {String} The URL of a script to load.
110      * @memberOf R.engine.Script
111      */
112     loadScript:function (scriptPath) {
113         // Put script into load queue
114         R.engine.Script.scriptQueue.push(scriptPath);
115         R.engine.Script.runScriptQueue();
116     },
117 
118     /**
119      * Low-level method to call jQuery to use AJAX to load
120      * a file asynchronously.  If a failure (such as a 404) occurs,
121      * it shouldn't fail silently.
122      *
123      * @param path {String} The url to load
124      * @param data {Object} Optional arguments to pass to server
125      * @param callback {Function} The callback method
126      * @memberOf R.engine.Script
127      */
128     ajaxLoad:function (path, data, callback) {
129         /* pragma:DEBUG_START */
130         // If we're in debug mode, force the browser to grab the latest
131         if (R.Engine.getDebugMode() && R.engine.Script.isMakeUnique()) {
132             path += (path.indexOf("?") == -1 ? "?" : "&") + "_debug=" + R.now();
133         }
134         /* pragma:DEBUG_END */
135 
136         // Use our own internal method to load a file with the JSON
137         // data.  This way, we don't fail silently when loading a file
138         // that doesn't exist.
139         var xhr = new XMLHttpRequest();
140         xhr.open("GET", path, true);
141         xhr.onreadystatechange = function (evt) {
142             if (xhr.readyState == 4) {
143                 callback(xhr, xhr.status);
144             }
145         };
146         var rData = null;
147         if (data) {
148             rData = "";
149             for (var i in data) {
150                 rData += (rData.length == 0 ? "?" : "&") + i + "=" + encodeURIComponent(data[i]);
151             }
152         }
153         xhr.send(rData);
154     },
155 
156     /**
157      * Load text from the specified path.
158      *
159      * @param path {String} The url to load
160      * @param data {Object} Optional arguments to pass to server
161      * @param callback {Function} The callback method which is passed the
162      *        text and status code (a number) of the request.
163      * @memberOf R.engine.Script
164      */
165     loadText:function (path, data, callback) {
166         if (typeof data == "function") {
167             callback = data;
168             data = null;
169         }
170         R.engine.Script.ajaxLoad(path, data, function (xhr, result) {
171             callback(xhr.responseText, xhr.status);
172         });
173     },
174 
175     /**
176      * Load text from the specified path and parse it as JSON.
177      *
178      * @param path {String} The url to load
179      * @param data {Object} Optional arguments to pass to server
180      * @param callback {Function} The callback method which is passed the
181      *        JSON object and status code (a number) of the request.
182      * @memberOf R.engine.Script
183      */
184     loadJSON:function (path, data, callback) {
185         if (typeof data == "function") {
186             callback = data;
187             data = null;
188         }
189         R.engine.Script.ajaxLoad(path, data, function (xhr, result) {
190             var json = null;
191             if (result != 404) {
192                 try {
193                     // Remove comments
194                     json = R.engine.Support.parseJSON(xhr.responseText);
195                 } catch (ex) {
196                     R.debug.Console.error("Error parsing JSON at '" + path + "'");
197                 }
198             }
199             callback(json, xhr.status);
200         });
201     },
202 
203     /**
204      * Internal method which runs the script queue to handle scripts and functions
205      * which are queued to run sequentially.
206      * @private
207      * @memberOf R.engine.Script
208      */
209     runScriptQueue:function () {
210         if (!R.engine.Script.scriptQueueTimer) {
211             // Process any waiting scripts
212             R.engine.Script.scriptQueueTimer = setInterval(function () {
213                 if (R.engine.Script.queuePaused) {
214                     if (R.engine.Script.pauseReps++ > 500) {
215                         // If after ~5 seconds the queue is still paused, unpause it and
216                         // warn the user that the situation occurred
217                         R.debug.Console.error("Script queue was paused for 5 seconds and not resumed -- restarting...");
218                         R.engine.Script.pauseReps = 0;
219                         R.engine.Script.pauseQueue(false);
220                     }
221                     return;
222                 }
223 
224                 R.engine.Script.pauseReps = 0;
225 
226                 if (R.engine.Script.scriptQueue.length > 0) {
227                     R.engine.Script.processScriptQueue();
228                 } else {
229                     // Stop the queue timer if there are no scripts
230                     clearInterval(R.engine.Script.scriptQueueTimer);
231                     R.engine.Script.scriptQueueTimer = null;
232                 }
233             }, 10);
234 
235             R.engine.Script.readyForNextScript = true;
236         }
237     },
238 
239     /**
240      * Put a callback into the script queue so that when a
241      * certain number of files has been loaded, we can call
242      * a method.  Allows for functionality to start with
243      * incremental loading.
244      *
245      * @param cb {Function} A callback to execute
246      * @memberOf R.engine.Script
247      */
248     setQueueCallback:function (cb) {
249         // Put callback into load queue
250         R.engine.Script.scriptQueue.push(cb);
251         R.engine.Script.runScriptQueue();
252     },
253 
254     /**
255      * You can pause the queue from a callback function, then
256      * unpause it to continue processing queued scripts.  This will
257      * allow you to wait for an event to occur before continuing to
258      * to load scripts.
259      *
260      * @param state {Boolean} <tt>true</tt> to put the queue processor
261      *                        in a paused state.
262      * @memberOf R.engine.Script
263      */
264     pauseQueue:function (state) {
265         R.engine.Script.queuePaused = state;
266     },
267 
268     /**
269      * Process any scripts that are waiting to be loaded.
270      * @private
271      * @memberOf R.engine.Script
272      */
273     processScriptQueue:function () {
274         if (R.engine.Script.scriptQueue.length > 0 && R.engine.Script.readyForNextScript) {
275             // Hold the queue until the script is loaded
276             R.engine.Script.readyForNextScript = false;
277 
278             // Get next script...
279             var scriptPath = R.engine.Script.scriptQueue.shift();
280 
281             // If the queue element is a function, execute it and return
282             if (typeof scriptPath === "function") {
283                 scriptPath();
284                 R.engine.Script.readyForNextScript = true;
285                 return;
286             }
287 
288             R.engine.Script.doLoad(scriptPath);
289         }
290     },
291 
292     /**
293      * This method performs the actual script loading.
294      * @private
295      * @memberOf R.engine.Script
296      */
297     doLoad:function (scriptPath, simplePath, cb) {
298         if (!R.Engine.started) {
299             return;
300         }
301 
302         var s = scriptPath.replace(/[\/\.]/g, "_");
303         if (R.engine.Script.loadedScripts[s] == null) {
304             // Store the request in the cache
305             R.engine.Script.loadedScripts[s] = scriptPath;
306 
307             R.engine.Script.scriptLoadCount++;
308             R.engine.Script.updateProgress();
309 
310             // If there's a callback for the script, store it
311             if (cb) {
312                 R.debug.Console.log("Push callback for ", simplePath);
313                 R.engine.Script.callbacks[simplePath] = cb;
314             }
315 
316             /* pragma:DEBUG_START */
317             // If we're in debug mode, force the browser to grab the latest
318             if (R.Engine.getDebugMode() && R.engine.Script.isMakeUnique()) {
319                 scriptPath += (scriptPath.indexOf("?") == -1 ? "?" : "&") + "_debug=" + R.now();
320             }
321             /* pragma:DEBUG_END */
322 
323             if (R.browser.Wii) {
324 
325                 $.get(scriptPath, function (data) {
326 
327                     // Parse script code for syntax errors
328                     if (R.engine.Linker.parseSyntax(data)) {
329                         var n = document.createElement("script");
330                         n.type = "text/javascript";
331                         $(n).text(data);
332 
333                         var h = document.getElementsByTagName("head")[0];
334                         h.appendChild(n);
335                         R.engine.Script.readyForNextScript = true;
336 
337                         R.engine.Script.scriptLoadCount--;
338                         R.engine.Script.updateProgress();
339                         R.debug.Console.debug("Loaded '" + scriptPath + "'");
340                     }
341 
342                 }, "text");
343             } else {
344 
345                 // We'll use our own script loader so we can detect errors (i.e. missing files).
346                 var n = document.createElement("script");
347                 n.src = scriptPath;
348                 n.type = "text/javascript";
349 
350                 // When the file is loaded
351                 var scriptInfo = {
352                     node:n,
353                     fullPath:scriptPath,
354                     simpPath:simplePath,
355                     callback:cb
356                 }, successCallback, errorCallback;
357 
358 
359                 successCallback = R.bind(scriptInfo, function () {
360                     if (!this.node.readyState ||
361                         this.node.readyState == "loaded" ||
362                         this.node.readyState == "complete") {
363 
364                         // If there was a callback, get it
365                         var callBack = R.engine.Script.callbacks[this.simpPath];
366 
367                         R.debug.Console.debug("Loaded '" + this.fullPath + "'");
368                         R.engine.Script.handleScriptDone();
369                         if ($.isFunction(callBack)) {
370                             R.debug.Console.info("Callback for '" + this.fullPath + "'");
371                             callBack(this.simpPath, R.engine.Script.SCRIPT_LOADED);
372 
373                             // Delete the callback
374                             delete R.engine.Script.callbacks[this.simpPath];
375                         }
376 
377                         if (!R.Engine.localMode) {
378                             // Delete the script node
379                             $(this.node).remove();
380                         }
381                     }
382                     R.engine.Script.readyForNextScript = true;
383                 });
384 
385                 // When an error occurs
386                 errorCallback = R.bind(scriptInfo, function (msg) {
387                     var callBack = this.callback;
388                     R.debug.Console.error("File not found: ", this.fullPath);
389                     if (callBack) {
390                         callBack(this.simpPath, R.engine.Script.SCRIPT_NOT_FOUND);
391                     }
392                     R.engine.Script.readyForNextScript = true;
393                 });
394 
395                 if (R.browser.msie) {
396                     n.defer = true;
397                     n.onreadystatechange = successCallback;
398                     n.onerror = errorCallback;
399                 } else {
400                     n.onload = successCallback;
401                     n.onerror = errorCallback;
402                 }
403 
404                 var h = document.getElementsByTagName("head")[0];
405                 h.appendChild(n);
406             }
407 
408         } else {
409             // Already have this script
410             R.engine.Script.readyForNextScript = true;
411         }
412     },
413 
414     /**
415      * Loads a game's script.  This will wait until the specified
416      * <tt>gameObjectName</tt> is available before running it.  Doing so will
417      * ensure that all dependencies have been resolved before starting a game.
418      * Also creates the default rendering context for the engine.
419      * <p/>
420      * All games should execute this method to start their processing, rather than
421      * using the script loading mechanism for engine or game scripts.  This is used
422      * for the main game script only.  Normally it would appear in the game's "index" file.
423      * <pre>
424      *  <script type="text/javascript">
425      *     // Load the game script
426      *     Engine.loadGame('game.js','Spaceroids');
427      *  </script>
428      * </pre>
429      * <p/>
430      * The game can provide configuration files which will be loaded and passed to the
431      * game's <tt>setup()</tt> method.  The name of the configuration file is the game
432      * as the game's main JavaScript file.  If your JavaScript file is "game.js", the
433      * format for the config files are:
434      * <ul>
435      *    <li><tt>game.config</tt> - General game configuration</li>
436      *    <li><tt>game_[browser].config</tt> - Browser specific configuration</li>
437      *    <li><tt>game_[browser]_[platform].config</tt> - Platform specific configuration</li>
438      * </ul>
439      * Examples: <tt>game_mobilesafari.config</tt>, <tt>game_mobilesafari_ipad.config</tt>
440      *
441      * @param gameSource {String} The URL of the game script.
442      * @param gameObjectName {String} The string name of the game object to execute.  When
443      *                       the framework if ready, the <tt>startup()</tt> method of this
444      *                       object will be called.
445      * @param [gameDisplayName] {String} An optional string to display in the loading dialog
446      * @memberOf R.engine.Script
447      */
448     loadGame:function (gameSource, gameObjectName/* , gameDisplayName */) {
449         if (!R.Engine.startup()) {
450             return;
451         }
452 
453         var gameDisplayName = arguments[2] || gameObjectName;
454 
455         $(document).ready(function () {
456             // Determine if the developer has provided a "loading" element of their own
457             if ($("span.loading").length == 0) {
458                 // They haven't, so create one for them
459                 $("head").append($(R.Engine.loadingCSS));
460 
461                 var loadingDialog = "<span id='loading' class='intrinsic'><table border='0' style='width:100%;height:100%;'><tr>";
462                 loadingDialog += "<td style='width:100%;height:100%;' valign='middle' align='center'><div class='loadbox'>Loading ";
463                 loadingDialog += gameDisplayName + "...<div id='engine-load-progress'></div><span id='engine-load-info'></span></div>";
464                 loadingDialog += "</td></tr></table></span>";
465 
466                 $("body", document).append($(loadingDialog));
467             }
468         });
469 
470         // We'll wait for the Engine to be ready before we load the game
471         // Load engine options for browsers
472         R.engine.Script.loadEngineOptions();
473 
474         // Load the config object for the game, if it exists
475         R.engine.Script.loadGameOptions(gameSource);
476 
477         R.engine.Script.gameLoadTimer = setInterval(function () {
478             if (R.engine.Script.optionsLoaded &&
479                 R.engine.Script.gameOptionsLoaded &&
480                 R.rendercontexts.DocumentContext &&
481                 R.rendercontexts.DocumentContext.started) {
482 
483                 // Show the virtual D-pad if the option is on
484                 R.engine.Support.showDPad();
485 
486                 // Start the engine
487                 R.Engine.run();
488 
489                 // Stop the timer
490                 clearInterval(R.engine.Script.gameLoadTimer);
491                 R.engine.Script.gameLoadTimer = null;
492 
493                 // Load the game
494                 R.debug.Console.debug("Loading '" + gameSource + "'");
495                 R.engine.Script.loadScript(gameSource);
496 
497                 // Start the game when it's ready
498                 if (gameObjectName) {
499                     R.engine.Script.gameRunTimer = setInterval(function () {
500                         var gameObj = R.getClassForName(gameObjectName);
501                         if (gameObj !== undefined && gameObj.setup) {
502                             clearInterval(R.engine.Script.gameRunTimer);
503 
504                             R.debug.Console.warn("Starting: " + gameObjectName);
505 
506                             // Remove the "loading" message (if we provided it)
507                             $("#loading.intrinsic").remove();
508 
509                             // Store the game object when it's ready
510                             R.Engine.$GAME = gameObj;
511 
512                             // Start the game
513                             gameObj.setup(R.engine.Script.gameOptionsObject);
514                         }
515                     }, 100);
516                 }
517             }
518         }, 2);
519     },
520 
521     /**
522      * Load the engine options object for the current browser and OS
523      * @memberOf R.engine.Script
524      * @private
525      */
526     loadEngineOptions:function () {
527         // Load the specific config for the browser type
528         R.engine.Script.optionsLoaded = false;
529 
530         // Load the options specific to the browser.  Whether they load, or not,
531         // the game will continue to load.
532         R.engine.Script.loadJSON(R.Engine.getEnginePath() + "/configs/" + R.engine.Support.sysInfo().browser + ".config", function (bData, status) {
533             if (status == 200 || status == 304) {
534                 R.debug.Console.debug("Engine options loaded for: " + R.engine.Support.sysInfo().browser);
535                 R.Engine.setOptions(bData);
536             } else {
537                 // Log an error (most likely a 404)
538                 R.debug.Console.log("Engine options for: " + R.engine.Support.sysInfo().browser + " responded with " + status);
539             }
540 
541             // Allow a game to override engine options
542             R.engine.Script.loadJSON("engine.config", function (bData, status) {
543                 if (status == 200 || status == 304) {
544                     R.debug.Console.debug("Engine option overrides loaded for game.");
545                     R.Engine.options = $.extend(R.Engine.options, bData);
546                 }
547 
548                 R.engine.Script.optionsLoaded = true;
549             });
550         });
551     },
552 
553     /**
554      * Load the the options object for the current game being loaded.
555      * @param gameSource {String} The game source file
556      * @memberOf R.engine.Script
557      * @private
558      */
559     loadGameOptions:function (gameSource) {
560         var file = gameSource.split(".")[0];
561         R.engine.Script.gameOptionsLoaded = false;
562         R.engine.Script.gameOptionsObject = {};
563 
564         // Attempt three loads for game options... First for the game in general, then
565         // for the browser, and finally for the browser and platform.  The objects will be
566         // merged together and passed to the setup() method of the game.
567         R.engine.Script.loadJSON(file + ".config", function (bData, status) {
568             if (status == 200 || status == 304) {
569                 R.debug.Console.debug("Game options loaded from '" + file + ".config'");
570                 R.engine.Script.gameOptionsObject = $.extend(R.engine.Script.gameOptionsObject, bData);
571             }
572 
573             // Now try to load a browser specific object
574             file += "_" + R.engine.Support.sysInfo().browser;
575             R.engine.Script.loadJSON(file + ".config", function (bData, status) {
576                 if (status == 200 || status == 304) {
577                     R.debug.Console.debug("Browser specific game options loaded from '" + file + ".config'");
578                     R.engine.Script.gameOptionsObject = $.extend(R.engine.Script.gameOptionsObject, bData);
579                 }
580 
581                 // Finally try to load a browser and platform specific object
582                 file += "_" + R.engine.Support.sysInfo().platform.toLowerCase();
583                 R.engine.Script.loadJSON(file + ".config", function (bData, status) {
584                     if (status == 200 || status == 304) {
585                         R.debug.Console.debug("Platform specific game options loaded from '" + file + ".config'");
586                         R.engine.Script.gameOptionsObject = $.extend(R.engine.Script.gameOptionsObject, bData);
587                     }
588 
589                     R.engine.Script.gameOptionsLoaded = true;
590                 });
591             });
592         });
593     },
594 
595     /**
596      * Load a script relative to the engine path.  A simple helper method which calls
597      * {@link #loadScript} and prepends the engine path to the supplied script source.
598      *
599      * @param scriptSource {String} A URL to load that is relative to the engine path.
600      * @memberOf R.engine.Script
601      */
602     load:function (scriptSource) {
603         R.engine.Script.loadScript(R.Engine.getEnginePath() + scriptSource);
604     },
605 
606     /**
607      * After a script has been loaded, updates the progress
608      * @private
609      * @memberOf R.engine.Script
610      */
611     handleScriptDone:function () {
612         R.engine.Script.scriptsProcessed++;
613         R.engine.Script.scriptRatio = R.engine.Script.scriptsProcessed / R.engine.Script.scriptLoadCount;
614         R.engine.Script.scriptRatio = R.engine.Script.scriptRatio > 1 ? 1 : R.engine.Script.scriptRatio;
615         R.engine.Script.updateProgress();
616     },
617 
618     /**
619      * Updates the progress bar (if available)
620      * @private
621      * @memberOf R.engine.Script
622      */
623     updateProgress:function () {
624         var pBar = jQuery("#engine-load-progress");
625         if (pBar.length > 0) {
626             // Update their progress bar
627             if (pBar.css("position") != "relative" || pBar.css("position") != "absolute") {
628                 pBar.css("position", "relative");
629             }
630             var pW = pBar.width();
631             var fill = Math.floor(pW * R.engine.Script.scriptRatio);
632             var fBar = jQuery("#engine-load-progress .bar");
633             if (fBar.length == 0) {
634                 fBar = jQuery("<div class='bar' style='position: absolute; top: 0px; left: 0px; height: 100%;'></div>");
635                 pBar.append(fBar);
636             }
637             fBar.width(fill);
638             jQuery("#engine-load-info").text(R.engine.Script.scriptsProcessed + " of " + R.engine.Script.scriptLoadCount);
639         }
640     },
641 
642     /**
643      * Load a stylesheet and append it to the document.  Allows for
644      * scripts to specify additional stylesheets that can be loaded
645      * as needed.  Additionally, you can use thise method to inject
646      * the engine path into the css being loaded.  Using the variable
647      * <tt>$<enginePath></tt>, you can load css relative to the
648      * engine's path.  For example:
649      * <pre>
650      *    .foo {
651     *       background: url('$<enginePath>/myGame/images/bar.png') no-repeat 50% 50%;
652     *    }
653      * </pre>
654      *
655      * @param stylesheetPath {String} Path to the stylesheet, relative to
656      *                                the engine path.
657      * @param relative {Boolean} Relative to the current path, or from the engine path
658      * @param noInject {Boolean} <code>true</code> to bypass engine path injection and use
659      *     a <tt><link /> tag to load the styles instead.
660      * @memberOf R.engine.Script
661      */
662     loadStylesheet:function (stylesheetPath, relative, noInject) {
663         stylesheetPath = (relative ? "" : R.Engine.getEnginePath()) + stylesheetPath;
664 
665         /* pragma:DEBUG_START */
666         // If we're in debug mode, force the browser to grab the latest
667         if (R.Engine.getDebugMode() && R.engine.Script.isMakeUnique()) {
668             stylesheetPath += (stylesheetPath.indexOf("?") == -1 ? "?" : "&") + "_debug=" + R.now();
669         }
670         /* pragma:DEBUG_END */
671 
672         var f = function () {
673             if (noInject) {
674                 $("head", document).append($("<link type='text/css' rel='stylesheet' href='" + stylesheetPath + "'/>"));
675             } else {
676                 $.get(stylesheetPath, function (data) {
677                     // process the data to replace the "enginePath" variable
678                     var epRE = /(\$<enginePath>)/g;
679                     data = data.replace(epRE, R.Engine.getEnginePath());
680                     if (R.engine.Support.sysInfo().browser == "msie") {
681                         // IE likes it this way...
682                         $("head", document).append($("<style type='text/css'>" + data + "</style>"));
683                     } else {
684                         $("head", document).append($("<style type='text/css'/>").text(data));
685                     }
686                     R.debug.Console.debug("Stylesheet loaded '" + stylesheetPath + "'");
687                 }, "text");
688             }
689         };
690 
691         R.engine.Script.setQueueCallback(f);
692     },
693 
694     /**
695      * Output the list of scripts loaded by the Engine to the console.
696      * @memberOf R.engine.Script
697      */
698     dumpScripts:function () {
699         for (var f in this.loadedScripts) {
700             R.debug.Console.debug(R.engine.Script.loadedScripts[f]);
701         }
702     },
703 
704     /**
705      * Clears the script name cache.  Allows scripts to be loaded
706      * again.  Use this method with caution, as it is not recommended
707      * to load a script if the object is in use.  May cause unexpected
708      * results.
709      * @memberOf R.engine.Script
710      */
711     clearScriptCache:function () {
712         R.engine.Script.loadedScripts = {};
713     },
714 
715     isMakeUnique:function () {
716         return R.engine.Script.uniqueRequest;
717     },
718 
719     setUniqueRequest:function (state) {
720         R.engine.Script.uniqueRequest = state;
721     }
722 
723 });
724