Wednesday, December 15, 2010

Minecraft Multiplayer teleporting with a server-side script!

So I play Minecraft with a few buddies on a stock (currently updated) Minecraft server on my Windows 7 box.  One of my friends decided to build a house way out in the middle of nowhere and I couldn't figure out how to get back there.  I decided to dig around in the Minecraft server directory structure, and found the player.dat files.  I assumed they held the players' location and inventory, so I tried reverse engineering them with a hex editor to determine a player's saved location when they were logged out, but it wasn't as obvious as I'd hoped.  I decided to get a little crazy, and just copy the player.dat file over my own, and log into the game.  Low and behold, I was standing in his place with his inventory!

That got me to thinking about ways I could do this automatically as a sort of teleportation.  I ended up coming up with this script:

#!C:\Perl\bin\perl -w

use IPC::Open2;
use strict;
use warnings;

my $minecraft_server_path = 'c:\MineCraft';
my $world_name = 'blockatopia';

my $pid = open2(*OUTPUT, *INPUT, 'java -Xmx1024M -Xms1024M -jar minecraft_server.jar nogui 2>&1');

while (my $line = <OUTPUT>){
    print $line;

    if ($line =~ /(\w+) issued server command: ([\w\s]+$)/){

        my $player_name = $1;
        my $command = $2;

        if (defined $command){
            if ($command =~ /^save secret location ([0-9a-zA-Z_]+)\s*$/){
                my $save_point_name = $1;

                #Kick the player so their dat file gets updated
                send_command("kick $player_name\n");

                #give the sever time to perform the kick
                sleep 1;

                #Make sure the location directory exists
                unless ( -e "$minecraft_server_path\\$world_name\\players\\secret_locations" ) {
                    system("mkdir $minecraft_server_path\\$world_name\\players\\secret_locations");
                }

                #Make a copy of that user's .dat file and rename it to the save location listed above
                system("copy $minecraft_server_path\\$world_name\\players\\$player_name.dat $minecraft_server_path\\$world_name\\players\\secret_locations\\$save_point_name.dat");
            }

            if ($command =~ /^save location ([0-9a-zA-Z_]+)\s*$/){
                my $save_point_name = $1;

                #Kick the player so their dat file gets updated
                send_command("kick $player_name\n");

                #give the sever time to perform the kick
                sleep 1;

                #Make sure the location directory exists
                unless ( -e "$minecraft_server_path\\$world_name\\players\\location" ) {
                    system("mkdir $minecraft_server_path\\$world_name\\players\\location");
                }

                #Make a copy of that user's .dat file and rename it to the save location listed above
                system("copy $minecraft_server_path\\$world_name\\players\\$player_name.dat $minecraft_server_path\\$world_name\\players\\location\\$save_point_name.dat");
            }

            if ($command =~ /^be ([0-9a-zA-Z_]+)\s*$/){
                my $save_point_name = $1;

                #Kick the player so they'll have to load it when they log back in
                send_command("kick $player_name\n");

                #give the sever time to perform the kick
                sleep 1;

                #Overwrite the user's dat file with the player's dat file if it exists
                if ( -e "$minecraft_server_path\\$world_name\\players\\$save_point_name.dat" ) {
                    system("copy $minecraft_server_path\\$world_name\\players\\$save_point_name.dat $minecraft_server_path\\$world_name\\players\\$player_name.dat");
                }
                else {
                    send_command("tell $player_name That player does not exist.\n");
                }
            }

            if ($command =~ /^secret warp ([0-9a-zA-Z_]+)\s*$/){
                my $save_point_name = $1;

                #Kick the player so they'll have to load it when they log back in
                send_command("kick $player_name\n");

                #give the sever time to perform the kick
                sleep 1;

                #Make sure the location directory exists
                unless ( -e "$minecraft_server_path\\$world_name\\players\\secret_locations" ) {
                    system("mkdir $minecraft_server_path\\$world_name\\players\\secret_locations");
                }

                #Overwrite the user's dat file with the saved location dat file if it exists
                if ( -e "$minecraft_server_path\\$world_name\\players\\secret_locations\\$save_point_name.dat" ) {
                    system("copy $minecraft_server_path\\$world_name\\players\\secret_locations\\$save_point_name.dat $minecraft_server_path\\$world_name\\players\\$player_name.dat");
                }
                else {
                    send_command("tell $player_name That location does not exist.\n");
                }
            }
           
            if ($command =~ /^warp ([0-9a-zA-Z_]+)\s*$/){
                my $save_point_name = $1;

                #Kick the player so they'll have to load it when they log back in
                send_command("kick $player_name\n");

                #give the sever time to perform the kick
                sleep 1;

                #Make sure the location directory exists
                unless ( -e "$minecraft_server_path\\$world_name\\players\\location" ) {
                    system("mkdir $minecraft_server_path\\$world_name\\players\\location");
                }

                #Overwrite the user's dat file with the saved location dat file if it exists
                if ( -e "$minecraft_server_path\\$world_name\\players\\location\\$save_point_name.dat" ) {
                    system("copy $minecraft_server_path\\$world_name\\players\\location\\$save_point_name.dat $minecraft_server_path\\$world_name\\players\\$player_name.dat");
                }
                else {
                    send_command("tell $player_name That location does not exist.\n");
                }
            }
           
            if ($command =~ /^list locations\s*$/){
                my $save_point_name = $1;

                #give the sever time to perform the kick
                sleep 1;

                #Make sure the location directory exists
                unless ( -e "$minecraft_server_path\\$world_name\\players\\location" ) {
                    system("mkdir $minecraft_server_path\\$world_name\\players\\location");
                }
               
                my $files = `dir /B $minecraft_server_path\\$world_name\\players\\location`;
                my @files = split "\n", $files;

                foreach my $file (@files) {
                    $file =~ s/\.dat//g;

                    send_command("tell $player_name [$file]\n");
                }
            }
            if ($command =~ /^help\s*$/){
                #give the server time to finish it's help command
                sleep 1;

                #Now send our help information
                send_command("tell $player_name /save location name_of_place\n");
                send_command("tell $player_name /warp name_of_place\n");
                send_command("tell $player_name /list locations\n");
            }
        }
    }
    if ($line =~ /poop/g){
        send_command("say Not allowed to say poop!\n");
        print "[Wrapper sent]say You said poop!\n";
    }
}

sub send_command {
    my $command = shift;

    print INPUT $command;
    print "[Wrapper sent]$command";
}


This script is a wrapper around the actual minecraft executable.  It uses a module to read the output of the server (all of player's entered commands and chatting) and based on strings that it sees, sends commands back to the server along with copying and moving files around on the server side.  Once this script is running your minecraft server, you can execute the following commands in game by hitting 't' and then one of the commands below:

/save secret location name_of_warp_location
/save location name_of_warp_location
/secret warp name_of_warp_location
/warp name_of_warp_location
/be player_name
/list locations


Command Definitions are as follows:

/save secret location name_of_warp_location - saves the current location (and inventory) of the player under the name specified by "name_of_warp_location", but does not add this location to the public list.  (See the /list command below)
/save location name_of_warp_location - saves the current location (and inventory) of the player under the name specified by "name_of_warp_location", and adds it to the public locations list (See the /list command below)

/secret warp name_of_warp_location - teleports the current player to the location specified (and gives them the inventory saved at that location).  Only works with secret locations
/warp name_of_warp_location - teleports the current player to the location specified (and gives them the inventory saved at that location).  Only works with non-secret locations

/be player_name - teleports the current player to the last known location of player_name (and gives current player a copy of everything that was in player_name's inventory)

/list locations - lists all publicly saved locations on this server.  (Also lists the users currently logged on because this command piggy-backs the normal /list command)
NOTE:  All of the "save location" commands will perform a kick of the player that performs them in order to force the players location to be saved in the back-end file before it gets copied.  Don't freak out when you get kicked, just log back in and the location will be in /list locations.  Also, all of the "warp" commands will kick a player before copying their file because the login process is the only way that I know of to load a player.dat file back into a player :).

Now, the more complicated problem of making Perl run on your windows machine is a different story.  I used the Active Perl installation from the folks over at ActiveState.com in order to get it running on my machine and it was fairly straight-forward.  If not, well..there are plenty of howto's on the internet that can help you out with that ;).  Once Perl's installed, just modify these two variables to match your Minecraft server location and world name:


my $minecraft_server_path = 'c:\MineCraft';
my $world_name = 'blockatopia';


For those of you who have a nice Linux box that will run the Minecraft server worth a darn (Sadly, mine is in dire need of an upgrade) this script should work just fine for you as long as you reverse all of the \\ to //.  I didn't feel like putting the time into this to make it truly cross-platform... my bad ;).

Hope this helps you all enjoy Minecraft a little more.  It's certainly made my time more "productive"...wait...scratch that ;)

1 comment:

  1. Hey I want to ask you a question. This post was cool, but I'm pursuing something different.

    I'm interested in figuring out how to issue commands into the JVM session itself via a command execution from outside the environment.

    Specific use case: seeking how to leverage an external log management tool to detect and correlate certain logged events, and respond with an execution response issuing commands utilizing variables set from the logged event.

    For example: define a certain area, and someone tries to grief it. Logged event! Event log is parsed, event is normalized, correlated, and response execution utilizes username to execute a megastrike.

    Wie? Because, like you, I am a nerd. Also, a father. Five kids. 15, 13, 3, and two twin girls, aged four months. In other words; NERD

    ReplyDelete