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 ;)

Sunday, December 12, 2010

She's five months old now...

...and boy has she changed.

I mentioned in my last post (yeah, two months ago) that I was hoping that the next post would be about Fiona laughing and playing more.  I'm proud to say that she absolutely is a happier baby these days. She can sit up on her own (after we put her there ;) ) but will eventually fall down.  Her legs are strong, (they have been for awhile) and she can hold up her own weight for a very long time as long as we're keeping her steady.  We're both wondering if she'll walk before she crawls.

She despises tummy time, but we've found that we can trick her into laying mostly on her belly with her new Tigger stuffed animal.  Nap times are much easier to get her into now, and we've become quite adept at telling when she needs one.  She's also an excellent sleeper at night.  Her bedtime is around 9:00 PM depending on our schedule and when she ate last.  Until two nights ago we were still tightly swaddling her in her crib.  We made the decision to stop based on her increasing ability to get her arms free.  It's been mostly un-eventful, except that she did wake up once each night a bit fussy.  We waited it out, and each time it only took about 60 seconds for her to fall back into a deep sleep.

Kim has officially stopped breast feeding which means Fiona managed to get almost 5 full months of breast milk.  It really broke her heart, and made her doubt herself as a mother (which was a silly thing to think in my opinion because it wasn't like she quit on purpose.  Her milk simply dried up. ;) ) but now Kim's able to sleep through the night instead of waking up to pump and we have much more freedom when we're out and about.  She's also enjoying her coffee and alcohol again..in moderation of course :). 

We recently got to experience our baby's first cold.  It wasn't severe, but it was definitely different to see how Fiona reacted to being under the weather.  Surprisingly, she wasn't fussy.  In fact, she honestly seemed to feel too bad to bother with being fussy.  Her facial expressions were blank a lot, and she just wanted to sleep.  Her poor stuffy nose was really difficult to work with.  It will be so much easier when we can just tell her to blow her nose when she's older.  We took the suggested precautionary steps that our pediatrician recommended for congestion which included buying a humidifier and keeping it in her room.  She may not have needed it, but I know from personal experience how dry air can turn a simple cold into bronchitus.  Thankfully, the little girl bounced back rather well, even with our holiday traveling and she seems to be much better now.

So those were the facts.  Here are the feelings.  Kim and I are both mentally exhausted.  We've not been comfortable allowing anyone else to babysit for us since Fiona has been born, and I think we're finally feeling the fatigue from not having any "us" time.  With my full time job, and Kim staying at home with her all day, we both feel like we work 100% of the time.  Weekends certainly don't mean what they used to, but with the two of us to split the load of caring for our daughter, the weekends ARE easier going than a normal work day. 

On an introspective note, I never really appreciated the extra load it puts on a person to be responsible for another human being.  Especially with one who's a bit higher maintenance than most.

The holidays certainly aren't helping our stress levels.  We're trying very hard to be aggressive about paying off debt so that we can move out of this house and into a new one that better suits our needs and wants.  Currently we're crammed into this one, and I think the tight spaces are getting to both of us.  Unfortunately, being aggressive about debt doesn't agree with holiday shopping, and we seem to have many more nieces, nephews, and cousins this Christmas to shop for.  It's got the two of us with pretty short fuses ;).  Thankfully we've been pretty good about talking our feelings out before either of us explodes.

We're trying to stay focused on the important things through all of this.  We want Fiona to participate in Christmas, even though we don't necessarily buy in on the same reasons that everyone else does so we have a Christmas tree up this year.  We also try to take her out to see family a lot, and we've started watching our language a lot more as I'm told she's a sponge at this stage ;).  We do our best to split up her day by playing with her directly, letting her play by herself, talking to her a lot, trying to make her laugh, and cuddling up close to her (which is what her and her mom are doing now ;) ) so that she experiences a wide variety of behaviors.  We don't want her to be co-dependent, nor do we want her to be a hermit, so we try very hard to balance her life out as much as we can and when we see a habit start to form that we feel is unhealthy (such as only wanting to nap in Mommy's lap ;) ), we do our best to nip it in the bud.  So far we're very happy and proud of her habits (especially her sleep habits ;) ).  I feel that Kim and I are a perfect match when it comes to how we want our children raised, and I'm very thankful that she and I are together to raise Fiona and our future children.

I won't make any empty promises that I'll post more frequently, because quite frankly it took some good timing to be able to write up this post.  Raising babies is time consuming and ever-interrupting.  Perhaps when she's old enough to entertain herself more I'll have more time to do frivolous things, but I'm not there yet so I don't know ;).

Thanks for reading...tune in next time :)