diff --git a/docs/API-Reference.md b/docs/API-Reference.md index ce30c3215..80d387d94 100644 --- a/docs/API-Reference.md +++ b/docs/API-Reference.md @@ -68,6 +68,8 @@ Walter Higgins * [utils.nicely() function](#utilsnicely-function) * [utils.at() function](#utilsat-function) * [utils.find() function](#utilsfind-function) + * [utils.serverAddress() function](#utilsserveraddress-function) + * [utils.watchFile() function](#utilswatchfile-function) * [Drone Plugin](#drone-plugin) * [TLDNR; (Just read this if you're impatient)](#tldnr-just-read-this-if-youre-impatient) * [Constructing a Drone Object](#constructing-a-drone-object) @@ -1338,6 +1340,34 @@ var jsFiles = utils.find('./', function(dir,name){ }); ``` +### utils.serverAddress() function + +The utils.serverAddress() function returns the IP(v4) address of the server. + +```javascript +var utils = require('utils'); +var serverAddress = utils.serverAddress(); +console.log(serverAddress); +``` +### utils.watchFile() function + +Watches for changes to the given file or directory and calls the function provided +when the file changes. + +#### Parameters + + * File - the file to watch (can be a file or directory) + * Callback - The callback to invoke when the file has changed. The callback takes the + changed file as a parameter. + +#### Example + +```javascript +var utils = require('utils'); +utils.watchFile( 'test.txt', function( file ) { + console.log( file + ' has changed'); +}); +``` ## Drone Plugin The Drone is a convenience class for building. It can be used for... @@ -2560,8 +2590,44 @@ quickly realise how to grant themselves and others operator privileges once they have access to ScriptCraft). The goal of this module is not so much to enforce restrictions -(security or otherwise) but to make it easier for tutors to setup a shared server -so students can learn Javascript. +(security or otherwise) but to make it easier for tutors to setup a +shared server so students can learn Javascript. When scripting is +turned on, every player who joins the server will have a dedicated +directory into which they can save scripts. All scripts in such +directories are automatically watched and loaded into a global +variable named after the player. + +So for example, if player 'walterh' joins the server, a `walterh` +global variable is created. If a file `greet.js` with the following +content is dropped into the `plugins/scriptcraft/players/walterh` +directory... + +```javascript +exports.hi = function( player ){ + player.sendMessage('Hi ' + player.name); +}; +``` + +... then it can be invoked like this: `/js walterh.hi( self )` . This +lets every player/student create their own functions without having +naming collisions. + +It's strongly recommended that the +`craftbukkit/plugins/scriptcraft/players/` directory is shared so that +others can connect to it and drop .js files into their student +directories. On Ubuntu, select the folder in Nautilus (the default +file browser) then right-click and choose *Sharing Options*, check the +*Share this folder* checkbox and the *Allow others to create and +delete files* and *Guest access* checkboxes. Click *Create Share* +button to close the sharing options dialog. Students can then access +the shared folder as follows... + + * Windows: Open Explorer, Go to \\{serverAddress}\players\ + * Macintosh: Open Finder, Go to smb://{serverAddress}/players/ + * Linux: Open Nautilus, Go to smb://{serverAddress}/players/ + +... where {serverAddress} is the ip address of the server (this is +displayed to whoever invokes the classroom.allowScripting() function.) ### classroom.allowScripting() function @@ -2569,6 +2635,11 @@ Allow or disallow anyone who connects to the server (or is already connected) to use ScriptCraft. This function is preferable to granting 'ops' privileges to every student in a Minecraft classroom environment. +Whenever any file is added/edited or removed from any of the players/ +directories the contents are automatically reloaded. This is to +facilitate quick turnaround time for students getting to grips with +Javascript. + #### Parameters * canScript : true or false diff --git a/src/main/js/lib/plugin.js b/src/main/js/lib/plugin.js index 448d0a240..5b7bec7b7 100644 --- a/src/main/js/lib/plugin.js +++ b/src/main/js/lib/plugin.js @@ -31,15 +31,7 @@ var _plugin = function(/* String */ moduleName, /* Object */ moduleObject, isPer exports.plugin = _plugin; -var scriptCraftDir = null; -var pluginDir = null; -var dataDir = null; - -exports.autoload = function( dir, logger ) { - - scriptCraftDir = dir; - pluginDir = new File( dir, 'plugins' ); - dataDir = new File( dir, 'data' ); +exports.autoload = function( context, pluginDir, logger, options ) { var _canonize = function( file ) { return '' + file.canonicalPath.replaceAll('\\\\','/'); @@ -76,22 +68,26 @@ exports.autoload = function( dir, logger ) { var len = sourceFiles.length; if ( config.verbose ) { - console.info( len + ' scriptcraft plugins found.' ); + console.info( len + ' scriptcraft plugins found in ' + pluginDir ); } for ( var i = 0; i < len; i++ ) { pluginPath = _canonize( sourceFiles[i] ); module = {}; try { - module = require( pluginPath ); + module = require( pluginPath , options); for ( property in module ) { /* - all exports in plugins become global + all exports in plugins become members of context object */ - global[property] = module[property]; + context[property] = module[property]; } } catch ( e ) { - logger.severe( 'Plugin ' + pluginPath + ' ' + e ); + if ( typeof logger != 'undefined' ) { + logger.severe( 'Plugin ' + pluginPath + ' ' + e ); + } else { + java.lang.System.out.println( 'Error: Plugin ' + pluginPath + ' ' + e ); + } } } }(pluginDir)); diff --git a/src/main/js/lib/require.js b/src/main/js/lib/require.js index fa1b91ed1..d23298a86 100644 --- a/src/main/js/lib/require.js +++ b/src/main/js/lib/require.js @@ -136,57 +136,56 @@ When resolving module names to file paths, ScriptCraft uses the following rules. ***/ var resolveModuleToFile = function ( moduleName, parentDir ) { - var file = new File(moduleName); - + var file = new File(moduleName), + i = 0, + pathWithJSExt, + resolvedFile; if ( file.exists() ) { return fileExists(file); } if ( moduleName.match( /^[^\.\/]/ ) ) { // it's a module named like so ... 'events' , 'net/http' // - var resolvedFile; - for (var i = 0;i < modulePaths.length; i++){ + for ( ; i < modulePaths.length; i++ ) { resolvedFile = new File(modulePaths[i] + moduleName); - if (resolvedFile.exists()){ + if ( resolvedFile.exists() ) { return fileExists(resolvedFile); - }else{ + } else { // try appending a .js to the end resolvedFile = new File(modulePaths[i] + moduleName + '.js'); - if (resolvedFile.exists()) + if ( resolvedFile.exists() ) { return resolvedFile; + } } } } else { // it's of the form ./path file = new File(parentDir, moduleName); - if (file.exists()){ + if ( file.exists() ) { return fileExists(file); - }else { - + } else { // try appending a .js to the end - var pathWithJSExt = file.canonicalPath + '.js'; - file = new File( parentDir, pathWithJSExt); - if (file.exists()) + pathWithJSExt = file.canonicalPath + '.js'; + file = new File( parentDir, pathWithJSExt ); + if (file.exists()) { return file; - else{ + } else { file = new File(pathWithJSExt); - if (file.exists()) + if ( file.exists() ) { return file; + } } } } return null; }; - /* - wph 20131215 Experimental - */ var _loadedModules = {}; var _format = java.lang.String.format; /* require() function implementation */ - var _require = function( parentFile, path ) { + var _require = function( parentFile, path, options ) { var file, canonizedFilename, moduleInfo, @@ -194,7 +193,15 @@ When resolving module names to file paths, ScriptCraft uses the following rules. head = '(function(exports,module,require,__filename,__dirname){ ', code = '', line = null; - + + if ( typeof options == 'undefined' ) { + options = { cache: true }; + } else { + if ( typeof options.cache == 'undefined' ) { + options.cache = true; + } + } + file = resolveModuleToFile(path, parentFile); if ( !file ) { var errMsg = '' + _format("require() failed to find matching file for module '%s' " + @@ -205,10 +212,12 @@ When resolving module names to file paths, ScriptCraft uses the following rules. throw errMsg; } canonizedFilename = _canonize(file); - + moduleInfo = _loadedModules[canonizedFilename]; if ( moduleInfo ) { - return moduleInfo; + if ( options.cache ) { + return moduleInfo; + } } if ( hooks ) { hooks.loading( canonizedFilename ); @@ -228,7 +237,9 @@ When resolving module names to file paths, ScriptCraft uses the following rules. var tail = '})'; code = head + code + tail; - _loadedModules[canonizedFilename] = moduleInfo; + if ( options.cache ) { + _loadedModules[canonizedFilename] = moduleInfo; + } var compiledWrapper = null; try { compiledWrapper = eval(code); @@ -258,8 +269,8 @@ When resolving module names to file paths, ScriptCraft uses the following rules. }; var _requireClosure = function( parent ) { - return function( path ) { - var module = _require( parent, path ); + return function( path, options ) { + var module = _require( parent, path , options); return module.exports; }; }; diff --git a/src/main/js/lib/scriptcraft.js b/src/main/js/lib/scriptcraft.js index 4d80c780a..a3a7ec0cd 100644 --- a/src/main/js/lib/scriptcraft.js +++ b/src/main/js/lib/scriptcraft.js @@ -491,7 +491,7 @@ function __onEnable ( __engine, __plugin, __script ) try { while ( (r = br.readLine()) !== null ) { code += r + '\n'; - } + } wrappedCode = '(' + code + ')'; result = __engine.eval( wrappedCode ); // issue #103 avoid side-effects of || operator on Mac Rhino @@ -566,7 +566,7 @@ function __onEnable ( __engine, __plugin, __script ) setup paths to search for modules */ var modulePaths = [ jsPluginsRootDirName + '/lib/', - jsPluginsRootDirName + '/modules/' ]; + jsPluginsRootDirName + '/modules/' ]; if ( config.verbose ) { logger.info( 'Setting up CommonJS-style module system. Root Directory: ' + jsPluginsRootDirName ); @@ -612,27 +612,44 @@ function __onEnable ( __engine, __plugin, __script ) global.__onCommand = function( sender, cmd, label, args) { - var jsArgs = []; - var i = 0; + var jsArgs = [], + i = 0, + jsResult, + result, + cmdName, + fnBody; for ( ; i < args.length ; i++ ) { jsArgs.push( '' + args[i] ); } - var result = false; - var cmdName = ( '' + cmd.name ).toLowerCase(); + result = false; + cmdName = ( '' + cmd.name ).toLowerCase(); if (cmdName == 'js') { result = true; - var fnBody = jsArgs.join(' '); + fnBody = jsArgs.join(' '); global.self = sender; global.__engine = __engine; try { - var jsResult = __engine.eval(fnBody); - if ( jsResult ) { - sender.sendMessage(jsResult); - } + if ( typeof eval == 'undefined' ) { + jsResult = __engine.eval(fnBody); + } else { + /* + nashorn + https://bugs.openjdk.java.net/browse/JDK-8034055 + */ + jsResult = eval( fnBody ); + } + if ( typeof jsResult != 'undefined' ) { + if ( jsResult == null) { + sender.sendMessage('(null)'); + } else { + sender.sendMessage(jsResult); + } + } } catch ( e ) { logger.severe( 'Error while trying to evaluate javascript: ' + fnBody + ', Error: '+ e ); + sender.sendMessage( 'Error while trying to evaluate javascript: ' + fnBody + ', Error: '+ e ); throw e; } finally { delete global.self; @@ -646,7 +663,7 @@ function __onEnable ( __engine, __plugin, __script ) return result; }; - plugins.autoload( jsPluginsRootDir, logger ); + plugins.autoload( global, new File(jsPluginsRootDir,'plugins'), logger ); /* wph 20140102 - warn if legacy 'craftbukkit/js-plugins' or 'craftbukkit/scriptcraft' directories are present */ @@ -655,21 +672,21 @@ function __onEnable ( __engine, __plugin, __script ) cbDir = new File(cbPluginsDir.canonicalPath).parentFile, legacyExists = false, legacyDirs = [new File( cbDir, 'js-plugins' ), - new File( cbDir, 'scriptcraft' )]; + new File( cbDir, 'scriptcraft' )]; for ( var i = 0; i < legacyDirs.length; i++ ) { if ( legacyDirs[i].exists() - && legacyDirs[i].isDirectory() ) { + && legacyDirs[i].isDirectory() ) { - legacyExists = true; + legacyExists = true; - console.warn('Legacy ScriptCraft directory %s was found. This directory is no longer used.', + console.warn('Legacy ScriptCraft directory %s was found. This directory is no longer used.', legacyDirs[i].canonicalPath); } } if ( legacyExists ) { console.info( 'Please note that the working directory for %s is %s', - __plugin, jsPluginsRootDir.canonicalPath ); + __plugin, jsPluginsRootDir.canonicalPath ); } })(); diff --git a/src/main/js/modules/utils/utils.js b/src/main/js/modules/utils/utils.js index 20f27de3b..d55d7dd3b 100644 --- a/src/main/js/modules/utils/utils.js +++ b/src/main/js/modules/utils/utils.js @@ -459,3 +459,79 @@ exports.find = function( dir , filter ) { recurse( dir, result ); return result; }; +/************************************************************************ +### utils.serverAddress() function + +The utils.serverAddress() function returns the IP(v4) address of the server. + +```javascript +var utils = require('utils'); +var serverAddress = utils.serverAddress(); +console.log(serverAddress); +``` +***/ +exports.serverAddress = function() { + var interfaces = java.net.NetworkInterface.getNetworkInterfaces(); + var current, + addresses, + current_addr; + while ( interfaces.hasMoreElements() ) { + current = interfaces.nextElement(); + if ( ! current.isUp() || current.isLoopback() || current.isVirtual() ) { + continue; + } + addresses = current.getInetAddresses(); + while (addresses.hasMoreElements()) { + current_addr = addresses.nextElement(); + if ( current_addr.isLoopbackAddress() ) + continue; + if ( current_addr instanceof java.net.Inet4Address) + return current_addr.getHostAddress(); + } + } + return null; +}; +/************************************************************************ +### utils.watchFile() function + +Watches for changes to the given file or directory and calls the function provided +when the file changes. + +#### Parameters + + * File - the file to watch (can be a file or directory) + * Callback - The callback to invoke when the file has changed. The callback takes the + changed file as a parameter. + +#### Example + +```javascript +var utils = require('utils'); +utils.watchFile( 'test.txt', function( file ) { + console.log( file + ' has changed'); +}); +``` +***/ +var filesWatched = {}; +exports.watchFile = function( file, callback ) { + var File = java.io.File; + if ( typeof file == 'string' ) { + file = new File(file); + } + filesWatched[file.canonicalPath] = { + callback: callback, + lastModified: file.lastModified() + }; +}; +function fileWatcher() { + for (var file in filesWatched) { + var fileObject = new File(file); + var lm = fileObject.lastModified(); + if ( lm != filesWatched[file].lastModified ) { + filesWatched[file].lastModified = lm; + filesWatched[file].callback(fileObject); + } + } + setTimeout( fileWatcher, 5000 ); +}; +setTimeout( fileWatcher, 5000 ); diff --git a/src/main/js/plugins/classroom/classroom.js b/src/main/js/plugins/classroom/classroom.js index a91257a03..729c1f7c5 100644 --- a/src/main/js/plugins/classroom/classroom.js +++ b/src/main/js/plugins/classroom/classroom.js @@ -1,4 +1,10 @@ -var foreach = require('utils').foreach; +var utils = require('utils'), + autoload = require('plugin').autoload, + logger = __plugin.logger, + foreach = utils.foreach, + watchFile = utils.watchFile, + playersDir = __dirname + '/../../players/', + serverAddress = utils.serverAddress(); /************************************************************************ ## Classroom Plugin @@ -14,8 +20,44 @@ quickly realise how to grant themselves and others operator privileges once they have access to ScriptCraft). The goal of this module is not so much to enforce restrictions -(security or otherwise) but to make it easier for tutors to setup a shared server -so students can learn Javascript. +(security or otherwise) but to make it easier for tutors to setup a +shared server so students can learn Javascript. When scripting is +turned on, every player who joins the server will have a dedicated +directory into which they can save scripts. All scripts in such +directories are automatically watched and loaded into a global +variable named after the player. + +So for example, if player 'walterh' joins the server, a `walterh` +global variable is created. If a file `greet.js` with the following +content is dropped into the `plugins/scriptcraft/players/walterh` +directory... + +```javascript +exports.hi = function( player ){ + player.sendMessage('Hi ' + player.name); +}; +``` + +... then it can be invoked like this: `/js walterh.hi( self )` . This +lets every player/student create their own functions without having +naming collisions. + +It's strongly recommended that the +`craftbukkit/plugins/scriptcraft/players/` directory is shared so that +others can connect to it and drop .js files into their student +directories. On Ubuntu, select the folder in Nautilus (the default +file browser) then right-click and choose *Sharing Options*, check the +*Share this folder* checkbox and the *Allow others to create and +delete files* and *Guest access* checkboxes. Click *Create Share* +button to close the sharing options dialog. Students can then access +the shared folder as follows... + + * Windows: Open Explorer, Go to \\{serverAddress}\players\ + * Macintosh: Open Finder, Go to smb://{serverAddress}/players/ + * Linux: Open Nautilus, Go to smb://{serverAddress}/players/ + +... where {serverAddress} is the ip address of the server (this is +displayed to whoever invokes the classroom.allowScripting() function.) ### classroom.allowScripting() function @@ -23,6 +65,11 @@ Allow or disallow anyone who connects to the server (or is already connected) to use ScriptCraft. This function is preferable to granting 'ops' privileges to every student in a Minecraft classroom environment. +Whenever any file is added/edited or removed from any of the players/ +directories the contents are automatically reloaded. This is to +facilitate quick turnaround time for students getting to grips with +Javascript. + #### Parameters * canScript : true or false @@ -42,51 +89,73 @@ Only ops users can run the classroom.allowScripting() function - this is so that don't try to bar themselves and each other from scripting. ***/ -var _store = { enableScripting: false }; +var _store = { enableScripting: false }, + File = java.io.File; + +function revokeScripting ( player ) { + foreach( player.getEffectivePermissions(), function( perm ) { + if ( (''+perm.permission).indexOf( 'scriptcraft.' ) == 0 ) { + if ( perm.attachment ) { + perm.attachment.remove(); + } + } + }); +} + +function grantScripting( player ) { + console.log('Enabling scripting for player ' + player.name); + var playerName = '' + player.name; + playerName = playerName.replace(/[^a-zA-Z0-9_\-]/g,''); + var playerDir = new File( playersDir + playerName ); + playerDir.mkdirs(); + player.addAttachment( __plugin, 'scriptcraft.*', true ); + var playerContext = {}; + autoload( playerContext, playerDir, logger, { cache: false }); + global[playerName] = playerContext; + + watchFile( playerDir, function( changedDir ){ + autoload(playerContext, playerDir, logger, { cache: false }); + }); + +/* + player.sendMessage('Create your own minecraft mods by adding javascript (.js) files'); + player.sendMessage(' Windows: Open Explorer, go to \\\\' + serverAddress + '\\players\\' + player.name); + player.sendMessage(' Macintosh: Open Finder, Go to smb://' + serverAddress + '/players/' + player.name); + player.sendMessage(' Linux: Open Nautilus, Go to smb://' + serverAddress + '/players/' + player.name); +*/ + +} + var classroom = plugin('classroom', { allowScripting: function (/* boolean: true or false */ canScript, sender ) { - if ( typeof sender == 'undefined' ) { + sender = utils.player(sender); + if ( !sender ) { console.log( 'Attempt to set classroom scripting without credentials' ); console.log( 'classroom.allowScripting(boolean, sender)' ); return; } - if ( !sender.op ) { - console.log( 'Attempt to set classroom scripting without credentials: ' + sender.name ); - return; - } - /* only operators should be allowed run this function */ - if ( !sender.isOp() ) { + if ( !sender.op ) { + console.log( 'Attempt to set classroom scripting without credentials: ' + sender.name ); + sender.sendMessage('Only operators can use this function'); return; } - if ( canScript ) { - foreach( server.onlinePlayers, function( player ) { - player.addAttachment( __plugin, 'scriptcraft.*', true ); - }); - } else { - foreach( server.onlinePlayers, function( player ) { - foreach( player.getEffectivePermissions(), function( perm ) { - if ( (''+perm.permission).indexOf( 'scriptcraft.' ) == 0 ) { - if ( perm.attachment ) { - perm.attachment.remove(); - } - } - }); - }); - } + foreach( server.onlinePlayers, canScript ? grantScripting : revokeScripting); _store.enableScripting = canScript; + + sender.sendMessage('Scripting turned ' + ( canScript ? 'on' : 'off' ) + + ' for all players on server ' + serverAddress); }, store: _store }, true); exports.classroom = classroom; -events.on( 'player.PlayerLoginEvent', function( listener, event ) { - var player = event.player; +events.on( 'player.PlayerJoinEvent', function( listener, event ) { if ( _store.enableScripting ) { - player.addAttachment( __plugin, 'scriptcraft.*', true ); + grantScripting(event.player); } }, 'HIGHEST');