Using Scratch with Minecraft & Scriptcraft – step by step instructions

As described in my previous posts, I found  Scriptcraft, the Minecraft plugin created by Walter Higgins, to be a very powerful tool for Minecraft modding and also for learning Javascript. I use it myself to learn and also for teaching kids to code as they can see immediately the result of the issued commands. One of the features I love is that it implements a drone, described as “an (invisible) object you create every time you execute any of the building or movement functions”, that is very similar to the turtle from other languages.

While kids aged 10 or more can understand how it works and can learn how to create their own functions, younger ones can be overwhelmed by the syntax rules and the need to create the files or to link additional modules.

I’m teaching coding to some 8 years old kids who are pretty good with Scratch and love playing Minecraft so I searched for a solution to make these apps work together. The first configuration, described in Using Scratch & Scriptcraft (& more) to teach kids programming in Minecraft was very complex, using Scratch offline with experimental extensions, Node RED, mosquitto mqtt broker and Spigot Minecraft server with Scriptcraft with mqtt plugin. It was based  on Dave Locke’s idea for sending commands from Scratch as MQTT messages, presented in  Visual programming and integration with Scratch, Node-Red and MQTT.

Recently I found a simple solution to receive http commands and to pass them to Minecraft server, based on Node.js,  presented by Kevin Whinnery   in Child Processes, Streams, and Minecraft Server Management via Text Message using Node.js. This configuration only needs Node.js to link Scratch with Minecraft, where the commands are interpreted and executed by Scriptcraft.

I will try to present here more in detail all what is needed (you can skip any of the sections below if you already installed the related application):

On the server side:

Spigot MC server

You have to download BuildTools.jar from https://hub.spigotmc.org/jenkins/job/BuildTools/ and the to follow the instructions from https://www.spigotmc.org/wiki/buildtools/. Basically you have to run this command (not to double click on the downloaded file)

java -jar BuildTools.jar

and after a while you will find in the same folder craftbukkit-1.xx.jar and spigot-1.xx.jar. When writing this, latest version is 1.10, so I used the file spigot-1.10.jar.

The next steps are described at https://www.spigotmc.org/wiki/spigot-installation/. Follow them and make sure the server works.

Scriptcraft

Download scriptcraft.jar from http://scriptcraftjs.org/download/latest/ and copy it in the Minecraft server \plugins subfolder.
Restart the server and you will see something similiar to this in the output console:
[14:04:25 INFO]: [scriptcraft] Enabling scriptcraft v3.2.0-2016-03-19

Scriptcraft will create its own folder structure under the Minecraft server main folder.

Node.js

Download the pre-built installer for your OS from https://nodejs.org/en/download/, launch it and follow the on-screen instructions.

On the client side

Scratch 2.0 offline

Download it from https://scratch.mit.edu/scratch2download/  (start by installing Adobe Air if it isn’t already installed on your computer)

Minecraft client

Install the PC/Mac version from https://minecraft.net/en/download/. You must have a paid account in order to use it.

Making it all work together

Update: you can find the most recent version of the files below (mc.js, scratch.js and MCextension.json) at https://github.com/mpatrascu/ScratchMC/

Following the installation instructions for MC server you should have created a script or a batch file (depending on the OS) containing a line similar to this (I didn’t use the other parameters):

java -Xms512M -Xmx1G -jar spigot.jar

Instead of using this script to launch the server, you must create a mc.js file in the same folder, where you copy the following text (taking care to keep your previous parameters for spawning the new java process):

 

// Require Node.js standard library function to spawn a child process
var spawn = require('child_process').spawn;

// Create a child process for the Minecraft server using the same java process
// invocation we used manually before
var minecraftServerProcess = spawn('java', [
    '-Xmx1G',
    '-Xms512M',
    '-jar',
    'spigot.jar'] );

// Listen for events coming from the minecraft server process - in this case,
// just log out messages coming from the server
function log(data) {
    process.stdout.write(data.toString());
}
minecraftServerProcess.stdout.on('data', log);
minecraftServerProcess.stderr.on('data', log);


var http = require("http");

scratchResponse=[];  //stores the IP and current block values in MC for each Scratch client: 

http.createServer(function(request, response) { //communicates with Scratch extension on port 8088
var ip = request.connection.remoteAddress.slice(7);

if (request.url!="/poll"){ //commands from Scratch - URL format is /command/first_param/second_param/../last_param/
    
    var command=request.url.toString().slice(1);
    minecraftServerProcess.stdin.write('js scratch("' + command + '","' + ip + '")' + '\n');
    response.end();
    
    if (request.url=="/reset_all"){ //end of Scratch session        
        position=-1;
        for(i=0;i<scratchResponse.length; i++){             if (scratchResponse[i].ip==ip){                 position=i;                                 }             }         if (position>-1) scratchResponse.splice(position,1);
        console.log('Scratch client on IP: ' + ip + ' disconnected. Remaining ' + scratchResponse.length + ' connections.');
        }    
  }
else{ //polling by Scratch
    bltype = -1;
    bldata = -1;
    result = -1;
    
    for(i=0;i<scratchResponse.length; i++){
        if (scratchResponse[i].ip==ip){
            bltype=scratchResponse[i].bltype;
            bldata=scratchResponse[i].bldata;
            result=scratchResponse[i].result;
        }
    }

    response.write("blockType " + bltype + "\nblockData "+bldata + "\nresult " + result);
    response.end();

    }
}).listen(8088);

http.createServer(function(request, response) {  //receives updates from Minecraft server on port 8089 - URL format is /blockType/blockData/IP/
params=request.url.toString().slice(1).split("/");
console.log("update information received: " + request.url.toString().slice(1));
ip=params[3];

found=0;
for (i=0; i < scratchResponse.length;i++){
    if(scratchResponse[i].ip==ip){
        scratchResponse[i].bltype=params[0];
        scratchResponse[i].bldata=params[1];
        scratchResponse[i].result=params[2];
        found=1;
    }
}
if (!found)
    scratchResponse.push({bltype:params[0], bldata: params[1], result: params[2], ip:params[3]});

response.write('ip'+(found?' found':' added to list'));
response.end();

}).listen(8089);
You invoke this file by typing
>node mc.js
It will launch Minecraft server as a child process and will listen to ports 8088 and 8089. On port 8088 it receives commands and poll requests from Scratch and on port 8089 it receives information from MC server – current block values and result of some operations. You can leave the ports unchanged, or you can choose other values, but you will need to also update the following two files.
In the Scriptcraft plugins folder you have to copy the file scratch.js which interprets commands passed by the Node.js script.
var Drone = require('/drone/drone').Drone
var foreach = require('utils').foreach

var JavaString = java.lang.String;
var http = require('http');
var utils = require('utils');

var scratchReturnAddress = 'http://127.0.0.1:8089/';
    
scratchClients=[]

function scratch(command,ip){
    var players = utils.players();    
    var cmd=command.split("/");
    var index=-1;    
    var drona='';

    for(i=0;i<scratchClients.length;i++){
        if (scratchClients[i].ip==ip){
            console.log("target player: "+scratchClients[i].playerName);
            index=i;
            drona=scratchClients[i].drone;
            targetPlayer=scratchClients[i].targetPlayer;
            }
        }
    
    if(cmd[0]=='connect'){
            console.log('trying to connect to player ' + cmd[1]);
            playerName=cmd[1];
            for (pl in players){  //use server.getPlayer(playerName)
                if (players[pl].name.toLowerCase()==playerName.toLowerCase()){
                    targetPlayer=players[pl];
                    drona=new Drone (targetPlayer.location);
                    if (index==-1){ //new player
                        scratchClients.push({playerName:playerName, targetPlayer:targetPlayer,drone:drona, ip:ip});
                        index=scratchClients.length-1;
                        echo(targetPlayer, "Connected to Scratch client on IP "+ip);
                        }
                    else { //update existing player
                        scratchClients[index].playerName=playerName;
                        scratchClients[index].targetPlayer=targetPlayer;
                        scratchClients[index].drone=drona;
                        }
                    }
                }   
            if (index==-1) { //use of other command before connecting to any player
                console.log('Player not found');
                scratchReturn(-1, -1, 'player_not_found', ip); 
                return
                }    
            else { //return to Scratch current block
                bl=drona.getBlock();
                scratchReturn(bl.typeId, bl.data, 'connected', ip); //updates values for current block to be read by Scratch     
                }
            }                
        
    if (index==-1) { //use of other command before connecting to any player
        console.log('No target player defined');
        scratchReturn(-1, -1, 'no_target_player', ip); 
        return
        }
    console.log("command received -" + cmd[0]+";");
    switch(cmd[0]){        
         case 'boxCommand':   
         case 'box':    
                 if(cmd[1]==64){
                    drona.door(cmd[1]);
                        }
                else
                    drona.box(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]),parseInt(cmd[5]));                
            break;
        case 'box0':
        case 'box0Command':               
                drona.box0(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]),parseInt(cmd[5]));              
            break;
            
        case 'cylinder':
                drona.cylinder(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]));              
            break;    
        case 'cylinder0':
                drona.cylinder0(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]));              
            break;                
            
        case 'prism':
                drona.prism(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]));              
            break;    
        case 'prism':
                drona.prism(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]));              
            break;            
                    
        case 'rainbow':
                drona.rainbow(parseInt(cmd[1]));              
            break;    
            
        case 'bed':
                drona.bed();              
            break;
        
        case 'torch':
                drona.hangtorch();              
            break;

        case 'stairs':
                drona.stairs(''+cmd[1]+':'+parseInt(cmd[2]),parseInt(cmd[3]),parseInt(cmd[4]));              
            break;            

        case 'wallsign':
            textArray=[];
            for (i=1; i<5; i++)
                textArray.push(cmd[i]);
            drona.wallsign(textArray);              
            break;    
            
        case 'summon':   //summons mobs using Scriptcraft predefined drone's summon method
            console.log("summon "+cmd[1]);
            drona.spawn(cmd[1]) ;           
            break ;
            
            
        case 'moveDrone':
            console.log("before moveDrone: " + parseInt(10*drona.x)/10 + ":" + parseInt(10*drona.y)/10 + ":" + parseInt(10*drona.z)/10);
            switch(cmd[1]){    
                case 'reset':
                    drona=new Drone (targetPlayer.location);
                    scratchClients[index].drone=drona;               
                    break;
                case 'up':
                    drona.up(parseInt(cmd[2]));                    
                    break;
                case 'down':
                    drona.down(parseInt(cmd[2])); 
                    break;
                case 'left':
                    drona.left(parseInt(cmd[2])); 
                    break;
                case 'right':
                    drona.right(parseInt(cmd[2])); 
                    break;
                case 'fwd':
                    drona.fwd(parseInt(cmd[2])); 
                    break;
                case 'back':
                    drona.back(parseInt(cmd[2])); 
                    break;
                case 'turn':
                    drona.turn(parseInt(cmd[2])); 
                    break;
                case 'save_chkpt':
                    drona.chkpt(parseInt(cmd[2])); 
                    break;
                case 'goto_chkpt':
                    drona.move(parseInt(cmd[2])); 
                    break;
                }
            //console.log("after moveDrone: " + parseInt(100*drona.x)/100 + ":" + parseInt(100*drona.y)/100 + ":" + parseInt(100*drona.z)/100);            
        bl=drona.getBlock();
        scratchReturn(bl.typeId, bl.data, 'ok', ip); //updates values for current block to be read by Scratch
        break;
    }   
};



function scratchReturn(bltype, bldata, result, ip){ //updates values for current block to be read by Scratch
    var http = require('http');
    http.request( scratchReturnAddress + bltype + '/' + bldata + '/' + result + '/' +ip,function(responseCode, responseBody){console.log(  responseBody );});
}

exports.scratch=scratch
It keeps track of IPs for each client so you can have multiple connections from different computers. You can leave 127.0.0.1 for scratchReturnAddress as the processes run on the same machine, and the port must be the same as the second one from mc.js (8089).
On the client you will have to use MCextension.json file for Scratch. The port must be the same as the first port defined in mc.js (8088), but you need to update the IP address to match the server’s address if you are using a separate computer. You should also update the default player, replacing playerName with the one you will use most frequently.
{
   "extensionName": "Minecraft",
   "extensionPort": 8088,
   "host":"127.0.0.1",
   "useHTTP": true, 
   "blockSpecs": [
     [" ", "Connect User %s", "connect","playerName"],
     [" ", "box blType:%n blData:%n width:%n height:%n depth:%n", "boxCommand", 1, 0, 1, 1, 1],
     [" ", "box0 blType:%n blData:%n width:%n height:%n depth:%n", "box0Command", 1, 0, 1, 1, 1],
     [" ", "prism blType:%n blData:%n width:%n depth:%n", "prism", 1, 0, 3, 1],
     [" ", "prism0 blType:%n blData:%n width:%n depth:%n", "prism0", 1, 0, 3, 1],
     [" ", "cylinder blType:%n blData:%n radius:%n height:%n", "cylinder", 1, 0, 5, 1],
     [" ", "cylinder0 blType:%n blData:%n radius:%n height:%n", "cylinder0", 1, 0, 5, 1],    
     [" ", "stairs blType:%n blData:%n width:%n height:%n", "stairs", 1, 0, 1, 1],    
     [" ", "rainbow radius:%n", "rainbow", 12],     
     [" ", "place bed", "bed"],
     [" ", "hang torch", "torch"],
     [" ", "wallsign message :%s :%s :%s :%s", "wallsign", "", "", "", ""],
     [" ", "move drone %m.commands %n", "moveDrone", "fwd", 1],
     [" ", "summon %m.entities", "summon", "Chicken"],
     ["r", "blockType", "blockType"],
     ["r", "blockData", "blockData"],
     ["r", "result", "result"]
   ],
   
   "menus":{
        "commands": ["fwd", "back", "left", "right", "up", "down", "turn", "reset", "save_chkpt", "goto_chkpt"],
        "entities": ["Chicken", "Cow", "Wolf", "Pig", "Sheep", "Rabbit", "Horse", "Ocelot", "Villager", "Zombie", "Skeleton", "Creeper", "Spider"]
   }
 
}
Pressing Shift and clicking File in Scratch will provide access to Import experimental HTTP extension (the last option in the list) for opening the MCextension.json file.
By doing this, in the More blocks category you will have access to new blocks and variables. Before issuing any other command you should use Connect User and the name of a player with an open MC session on the server as a parameter. When connected, it will put a message in Minecraft chat and will automatically create a drone at player’s position, which will execute all the commands.
The new blocks defined in MCextension.json file try to replicate some of the functions that Walter Higgins defined in the drone plugin. For short descriptions of those functions and needed parameters please check https://github.com/walterhiggins/ScriptCraft/blob/master/docs/API-Reference.md#drone-plugin.
The box, prism, cylinder and rainbow are used to create solid structures, while box0, prism0 and  cylinder0  are for hollow structures – just walls without ceiling and floor.You can specify the material – block type and block data – and the dimensions.
Stairs, bed, torch and wallsign are used to place specific elements which must take into account player’s orientation.
The move drone function tells the drone where to move. The last drone position will be remembered so if you want to start again from the player’s position, you have to use move drone reset or Connect user again.
Summon is used to spawn the mobs from the predefined list. You can update this list for new mobs.
At the end of the file there are some variables updated by the Minecraft server:  blockType and blockData contain information about the block where the drone is, while result contains the result of last executed command.
If you want to check the values for blockType and blockData, you should insert a delay  of at least 0.2 secs (using standard Scratch block) after the last move drone instruction before using them.

Known issues

Sometimes commands are not executed in order, especially when the server is busy. It’s better to avoid launching command sequences from more than one computer at the same time, although it could work.
move drone save_chkpt and move drone goto_chkpt don’t work yet.

Future developments

Hunting for bugs, adding new functions, showing the equivalent Javascript commands on screen.

 

Advertisements

9 thoughts on “Using Scratch with Minecraft & Scriptcraft – step by step instructions

  1. I cannot seem to get Scratch to communicate with our server. When I click the Connect block in Scratch, I do not get a message on the server that it is connecting to it. What might be the issue? Just to let you know that I disabled the Windows Firewall to see if that was the issue, but it still did not work.

  2. You can try to connect from a browser on the client. Put in the address bar server_ip:8088/test/ and you should see in the server’s log
    [20:56:18 INFO]: [scriptcraft] command received -test;
    If you see something like this:
    [21:01:50 INFO]: Error while trying to evaluate javascript: scratch(“test/”,”127.0.0.1″), Error: javax.script.ScriptException: ReferenceError: “scratch” is not defined in at line number 1,
    it means there is an error related to scratch() function. The file must be in \scriptcraft\plugins under the MC server main folder

  3. Thanks for the very detailed instructions. I almost got it work. There seems to be a couple of missing characters in your mc.js scratch.js example codes above. Could you please put the needed codes as files or update the code in the blog text?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s