Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

MineCraft Server Management

4.70/5 (12 votes)
28 Apr 2013CDDL29 min read 56.9K   2.7K  
A wrapper application making management, modding and running multiple MineCraft servers faster and easier.

This project has grown rapidly and has now been shifted to CodePlex! Head over there for the latest releases and code!

This project now has it's own official website at ManageMinecraft.com

Note: Program will only run on 64-bit OS (or one with 64-bit emulator ability)
Note: Program must be compiled in 64-bit configuration - in VS2010 go to Build - Configuration Manger... - Then set one up in 64 bit. For reason see Points of Interest at bottom.

Image 1

Introduction

The application I have developed makes management of multiple MineCraft servers much easier and faster wrapping the standard input/output with user friendly command buttons while still allowing expansion and the full capability of MineCraft. The application solves the issue of learning some of the most common commands for the MineCraft server and makes editing settings more reliable too.

Background

I only recently started playing MineCraft and it quickly became apparent that the best form was playing online with my friends. This meant having a server and since I have a good internet connection and a bit of nouse when it comes to computers, I decided to set up a server. However, it I quickly got fed up of trying to remember commands and managing settings such as the white-list. I decided, therefore, that an easy-to-use management program would be the best solution and so here it is.

Using the code

The simple way to use the code is of course to download the application and start it up. The user interface should be fairly self explanatory and easy to use with controls enabling and disabling when they can/can't be used. I have also included a basic scheduler system that lets you select what time you wish the server to start at/after and what time it should stop at/after. The application lets you select which folder contains all your servers' folders and then or without this step, which folder contains the server you wish to run. It will then let you select which of the available worlds you want to start with and what port the server should run on. The port number is loaded from the server's configuration so if you want to keep it the same there is n need to change it. Final things to note are the tab paged command buttons that can be added to or removed and the command line input at the bottom that allows input of any command and input of a command to add/remove from the command buttons.

Code Structure

The code has a basic but hopefully useful structure split into three two sections:

Back-end code:

  • MineCraft server class
  • Server Settings class

Front-end user interface:

  • Main Form
  • Command Arguments Form
  • Settings Form

The Classes

MineCraftServer class: This wraps up all the lowest level interaction with the MineCraft server itself (i.e. the Java application instance).

ServerSettings class: Allows easy editing/access to the major server settings files. Currently includes server.properties, ops.txt, white-list.txt and banned-players.txt. Support for other files such as banned-ips.txt could easily be added but I felt was somewhat less useful on the whole so haven't yet included. Future versions of this may.

OutputEvent.cs: Provides (very) basic event stuff for friendlier interaction with the MineCraft server output.

The Forms

Main Form: The main window that is loaded when the program starts. This does all initial loading of previous settings from the registry, allows selection of server for that instance and provides access to all the rest of the program (directly through buttons - no nasty menus, sub-menus nor windows after windows before you get to what you want.)

Command Arguments Form: Takes one argument for its constructor that allows specification of the text for the labels of the arguments the user must enter. Text boxes and labels are added dynamically based on these values. OK and cancel buttons are used and are already added to the form (not added dynamically) - dynamically hear meaning added by my own code not through IDE generated code.

Settings Form: Tab control for the different file plus a "General" tab that gives friendly controls for the main (almost all) server.properties settings (e.g. check boxes for enable PvP, Monsters etc.). Extensible and provides an "All" that allows any key/value pair (including comments where value is empty) to be added to the server.properties file - uses list box on this tab to display selectable keys.

How these work

MineCraftServer class:

The basic principle is to start up the MineCraft server in very much the same way that the launch.bat or MineCraft.exe programs would but then grab the output to display it in my program and redirect the input so my program can control it. This has been built into most console applications for a long time (so far as I can tell and at least on Windows) in the form of the StandardInput, StandardOutput and StandardError streams, the former being writeable as input, the latter two being readable output.

So, first things first, how do you start another application from C#? The answer is you use System.Diagnostics.Process and then the Start method. (I'm still looking for a satisfactory reason why this is placed under Diagnostics as it seems a bit odd - anyone know?) For this you need (or rather its preferable) to have created System.Diagnostics.ProcessStartInfo. The following code can be found in the Launch method.

C#
string JarFileName = GetJarFileName(ServerFolderAddress);
if (string.IsNullOrEmpty(JarFileName))
{ 
    throw new FileNotFoundException(/*Content removed for article.*/);
}
ProcessStartInfo ServerStartInfo = new ProcessStartInfo(@"java", 
                                      "-Xmx3G -Xms2G -jar " + JarFileName + " nogui -nojline")
{
    CreateNoWindow = true,
    UseShellExecute = false,
    RedirectStandardInput = true,
    RedirectStandardOutput = true,
    RedirectStandardError = true,
    WorkingDirectory = ServerFolderAddress,
};
if ((ServerProcess = Process.Start(ServerStartInfo)) != null) 

The first line you see is GetJarFileName. What this does is search for the existence of a MineCraft server jar file in the specified directory. However, since actually identifying the contents of a jar file is quite hard, I have done it based on the existence of a jar file with one of the following names (capitalisation of first letters of words and lack of underscores is accounted for.)

  • Tekkit.jar (will find tekkit.jar)
  • CraftBukkit.jar (will find craftBukkit.jar, Craftbukkit.jar and craftbukkit.jar)
  • Minecraft_server.jar (will find relevant permutations as seen above and likewise without underscore).

Note: The program will not allow you to use the MineCraft_server.exe file nor any launch.bat files.

The reason it doesn't? Well the .exe and .bat programs simply launch the jar file but with the correct command line arguments. I have done that in my program (basically doing what Minecraft.exe does). However, since they launch a new application, if we tried grabbing the Input/Ouput of the .exe or batch programs they wouldn't be helpful, we need the I/O of the actual Java program. Furthermore, to do this we need an extra argument passed to Java to make it work, something we couldn't do through the exe or bat programs.

So the code looks for a MineCraft server jar file, if one exists it passes back its file name (not full address name), if not is passes back null. Then a test is done to see if the jar file was found, if not throw an exception to tell the calling code (and thus user).

Next the code creates ProcessStartInfo. What this does is sets up all the arguments to pass to the file (specified by the file name/address) of the process we wish to start. It also does things like redirecting the Standard I/O streams which we need to control the server once it's started.

ProcessStartInfo accepts two parameters (in default constructor). These are: filename and arguments.

The filename is just "Java" since we just want to run the latest version of Java that is installed on the machine.

The arguments is where it gets interesting. The first argument ("-Xmx3G") specifies the maximum amount of memory Java should try and allocate to the program from RAM. 3G means 3 gigabytes and lower than this the server seems to refuse to run. The next argument is the same ("-Xms2G") but specifies the minimum amount to allocate - in this case 2 gigabytes. You can try fiddling and changing it to things like 1024M (M standing for megabytes) - notation for this is pretty simple and can be looked up easily.

The next argument ("-jar") tells Java it is a jar file we wish to open and then we specify the file name of the application to run - specifically file name not file address since we specify the working directory to start in later.

Almost we tell it to have no graphical user interface ("nogui") though this is not essential since we will specify not to create window for the process later.

Finally the most vital argument. For reasons unknown MineCraft (or perhaps it's Java itself) does not by default use the StandardOutput, StandardError or StandardInput streams, or at least it doesn't link them up. This argument is absolutely vital to make MineCraft (or Java) use them so that the program can grab the Input/Output of the server.

A final note on the exited event. While not strictly necessary it is a good way of cleaning up the process and clearing events. I also use it to set ServerRunning to false. I would recommend using this event whenever starting a process that you assume control of, as is done here.

ServerSettings class:

Please note that the class requires you to load settings before doing anything else. You cannot create settings files from scratch with it. This is a possible improvement and has been listed at the end.

The ServerSettings class provides quick easy access to the following server files:

  • server.properties : The main server properties file. Technically this is a YAML file (I think) but I treat it as a file with one key/value pair per line since no properties currently use any of the other YAML structure.
  • banned-players.txt : The list of usernames that are banned. Similar support could be added for banned-ips.txt but I felt banned-ips was less useful.
  • white-list.txt
  • ops.txt

The Serve<code>rSettings class contains an enumerable called SettingsFile - this is used to specify which settings file (excluding the server.properties file) you wish to access. This will be clearer later but I would like to say here that if you wish to extend the class to incorporate banned-ips.txt or similar then adding an option called IPBlackList would be sensible.

There are four basic operations that you can want to do to a settings file.

  1. Load it - Involves reading the file into memory/more use-able list or dictionary form
  2. Add to/edit it - Involves adding key/value pairs or just values to a dictionary/list or replacing existing values - these are very similar operations and can be done with the same syntax so I have grouped them in one.
  3. Remove from it - Involves deleting a key/value pair or value. While this can be done with the same for a key/value pair (set the key to null) it can't with a list so this is a separate operation.
  4. Save it - Involves saving the new (in memory) version to a file in the correct format.

1. Loading the settings files:

The code currently handles the four aforementioned files but the code can easily be used to load other files (as has been said). The code looks like the following and for ease of explanation I have put comments in the code otherwise it could get (I fear) confusing:

C#
//The file path to the server.properties file
string SettingsFilePath = Path.Combine(ServerFolderAddress, "server.properties");
//Check the file exists
if (File.Exists(SettingsFilePath))
{
    //If it does, create FileInfo for it to give us easy access to useful file information
    SettingsFileInfo = new FileInfo(SettingsFilePath);
    //Read all the text from the file
    SettingsDataString = File.ReadAllText(SettingsFileInfo.FullName);
    //Converts the text into a Dictionary<string, string> where comments 
    //(denoted by # at start) have no value, only key. 
    //Invalid lines that are not comments will (or at least should) be ignored.
    //1. Split the data into separate lines (without empty lines)
    //2. Convert each line into a key/value pair using '=' as the separator
    SettingsData = SettingsDataString.Split("\r\n".ToCharArray(), 
                                      StringSplitOptions.RemoveEmptyEntries).ToDictionary(
        //Get the key from the data
        delegate(string x)
        {
            //If the line is a comment, return the whole line
            if (x.Trim()[0] == '#')
            {
                return x;
            }
            //Otherwise, return just the bit before the equals sign - the key
            else
            {
                //Split by equals, return the first bit, 
                //remove white space from the beginning and end of it 
                return x.Split('=').First().Trim();
            }
        },
        //Get the value from the data
        delegate(string x)
        {
            //Split the data using equals sign
            string[] SplitStrings = x.Split('=');
            //Check if the key is actually a comment and that there is a value for this key. 
            //Empty values are accepted as the StringSplitOptions.RemoveEmptyEntries is not set
            //If this were an invalid entry (i.e. without 'key=value' but just 'key', 
            //it would be treated as an empty entry
            if (SplitStrings[0].Trim()[0] != '#' && SplitStrings.Length >= 2)
            {
                //Return just the value, 
                //removing any possible remnants of its file origin - i.e. new lines
                return SplitStrings[1].Trim().Replace("\r", "").Replace("\n", "");
            }
            else
            {
                //Return nothing since the whole comment is in the key
                return "";
            }
        });
}
//If we couldn't find the main settings file 
//something has gone very wrong since it is required by MineCraft
else
{
    //Hence throw an exception
    throw new FileLoadException("Could not find main, required settings file!");
}

//Get the path of the white list
string WhiteListFilePath = Path.Combine(ServerFolderAddress, "white-list.txt");
//Check the white list exists
if (File.Exists(WhiteListFilePath))
{
    //Create white list FileInfo
    WhiteListFileInfo = new FileInfo(WhiteListFilePath);
    //Load white list data into a string
    WhiteListDataString = File.ReadAllText(WhiteListFileInfo.FullName);
    //Load white list data into a list, split by new line, ignore empty lines.
    WhiteListData = WhiteListDataString.Split("\r\n".ToCharArray(), 
                                              StringSplitOptions.RemoveEmptyEntries).ToList();
} 

Since the black list (banned-players list) and ops (operators) list are structured the same way as the white list (allowed players list) I have not included them in the above code sample.

2. Adding to/editing in memory settings:

The code allows two ways for adding key/value pairs to the server.properties settings and one way of adding to the other settings lists.

Methods of adding key/value pairs:

  1. Setting the key to a value. The code overloads the square brackets operator to allow indexing by key of the server.properties settings. You can set a key that does not exist to value or an existing key to a new value. This method uses the next method when setting a previously unset key.
  2. Add(string key, string value) method. This is fairly self explanatory just by looking at it. It simply calls the underlying Dictionary object's Add method.

Method of adding a value:

Since the class allows adding to three different lists (black list, white list and ops list) all of which have the same format, the same method is used, you just need to specify which list. The method itself calls the relevant underlying List object's Add method. It also checks you are not trying to add duplicates and so will stop that. The method accepts to arguments, the second is the value, the first is one of the options from the previously mentioned SettingsFile enumerable - note: this argument should not be used as flags - only one will be handled.

Also possible is indexing into the different settings file using the square brackets operator. This takes two arguments, the first is (only one of) the SettingsFile enumerable options and the second is the integer index of the value in the list that you want. Errors will be thrown if index is out of range.

At this point it is worth pointing out that overloading the square brackets operator does not happen in the way you would with other operators such as +, -, /, *, etc. To overload it you simply need the following line followed by normal get/set (or just get) code:

C#
public Type this[Type key]

//or even (and as I have used):

public Type this[Type Option, Type key] // which has multiple arguments  

Note: Type can be any type you like, not just a generic type so it could be string for instance. Furthermore, each type can replaced with a different one - they don't all need to be the same.

Here follows how to do it with the <code><code>+ operator: (Clearly MSDN uses a Complex number class but it could be replaced with nay class you like.)

C#
public static Complex operator +(Complex c1, Complex c2) 
{
   return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
}
//Adapted from: http://msdn.microsoft.com/en-us/library/aa288467(v=vs.71).aspx

The difference is (so far as I can tell) due to the fact that square brackets are what is called Subscript operators but +, -, /, * etc. which are Unary operators (etc. also includes Binary operators). Further useful reading are the following two MSDN pages:

  1. http://msdn.microsoft.com/en-us/library/aa288467(v=vs.71).aspx - Operator Overloading Tutorial
  2. http://msdn.microsoft.com/en-us/library/5tk49fh2.aspx - Operator Overloading - Full list of operators

3. Removing from in memory settings:

I write in memory in the title of these sections because that's what it is - it would be misleading to write "Removing from settings files" since there is no file manipulation involved. If you looked at the file after calling these methods you would see no change - you must call Save to see the effects in the file.

To remove a key/value pair from the server.properties list there are again two options:

  1. Set the value of the key to null - this then uses the method below. Note: setting it to blank or white space will retain the key but set the value to empty or white space, only null will remove it from the list entirely. Code example: MySettings["HelloWorld"] = null;
  2. Call the Remove(string key) method. This calls the underlying Dictionary object's Remove method.

To remove an item from one of the other lists, use the Remove(SettingsFile file, string value) method - works in the equivalent way to the Add method.

4. Saving in memory settings to settings files:

This process is broken down into two stages. The first stage is transferring the list (and Dictionary) data to its correct string representation. The second stage is saving that text to the relevant files.

Producing string representations:

Easy things first, creating string forms of the white list, black list and ops list is very easy. Just loop through all the values adding them with a new line after each. It is done as follows:

C#
 //Clear the data string
BlackListDataString = "";
//Use LINQ to write a compact for loop method for the list.
BlackListData.ForEach(delegate(string AName)
{
    //For the sake of it use Environment.NewLine - equiv. of \n
    BlackListDataString += AName + Environment.NewLine;
}); 

I chose to use LINQ for the for each method simply because it is quick, easy and compact. It has no computational gain or loss for anything we are concerned with (you may argue that speed might be compromised or improved but it really wont be noticeable in this application.) I would like to take the opportunity to talk briefly about delegate functions. This is a powerful way of writing short functions in inline code without creating a whole new method, especially if the code is only going to be used once. However, recently I have noticed some code that uses the same delegate function copied around the place. I would strongly discourage this because it results in code that is hard to update/maintain as you have lots of repeated lines. I would recommend using delegates but with a word of caution - don't copy them around the place!

On to the very mildly more complex creation of a string representation of the server.properties dictionary information. I can't say as delegates with Dictionary objects is as friendly as it is with Lists. Code far more rapidly becomes unreadable so I chose to use a proper For-Each loop here. The code is:

C#
//Clear the data string
SettingsDataString = "";
//Loop through all the key value pairs
foreach (KeyValuePair<string, string> ASetting in SettingsData)
{
    //Is the pair a comment?
    if (ASetting.Key.Trim()[0] == '#')
    {
        //If so just add the key - value should be blank but if it isn't, 
        //ignore it as it's not wanted.
        SettingsDataString += ASetting.Key + Environment.NewLine;
    }
    else
    {
        //Else if it's not a comment, add the key, the equals separator 
        //then the value and finally a new line.
        SettingsDataString += ASetting.Key + "=" + ASetting.Value + Environment.NewLine;
    }
} 

You may wonder why I chose to call Trim when testing for comments but not adding the key? Well, given how the file is supposed to be YAML, users may wish to put in very particular values (with spaces) to produce the desired result. Therefore, testing for comments uses Trim, just as MineCraft would but adding the key/value I don't because any extra spaces may well be intentional. The code is designed to be reasonably long lasting for future versions of MineCraft.

OutputEvent.cs file:

This file wraps up the necessary class and delegate for my own custom event used in the server when output is received. A good tutorial on MSDN for how to create your own events can be found at:
http://msdn.microsoft.com/en-us/library/aa645739(v=vs.71).aspx
otherwise it should simply be said that this file contains an OutputEventArgs class that contains the text that has been received from the server.

Settings form:

The settings form can be accessed from the Main Form after a server folder has been selected. It takes one argument as its constructor and that is the address of the selected server folder. The Settings Form does not save any changes to settings file until you close it when you have the option to save or not.

What does it contain? A tab control with the following tabs:

  1. General - Has user-friendly controls for the main server.properties settings. This includes a numeric up down for server port; check boxes for boolean options such as PvP enabled, monsters enabled; text box for World Name (setting is level-name); drop down box for level type; text box for message of the day. The bottom of the form has a Set Values button in green - this must be clicked for the values you change to be set. Before changing to another tab, make sure you have clicked the Set Values button.
  2. All - Has a list box and key/value entry of settings found in server.properties file. Has set and remove buttons.
  3. Black List - Provides a list box, value entry box and add/remove buttons.
  4. White List - As with Black List.
  5. Ops - As with Black List.

The form code uses the ServerSettings class which has already been described. It loads all the settings when an instance of the form is created and gives the user a yes or no choice when it closes as to whether it saves any changes or not.

Main Form:

So now we get to the main form of the program, the one that links it all together.

Image 2
(Outdated image - This is of the first version of the program)

The form controls go down in a linear progression of use. They will also enable/disable when they can/can't be used. To start with you can pick a folder that contains all your MineCraft server sub folders, which will then be listed in the drop down box below. Or you can directly pick the MineCraft server folder you wish to use, skipping the above step. This is why there are two browse buttons. The program is designed to make switching between server very easy and fast. So, when you select a server to use the basic settings are loaded into the World and Port boxes. This allows you to quickly select which of the available worlds you wish to start the server running (box is a drop-down list which you can also type into so that a new world can be created). The Port number box is a numeric up down that allows quick selection of which port you want your server to run on. When you launch the server, these settings are first automatically saved to the server.properties file so the server launches with those settings. The form also obviously has the Server Settings button that open up the Settings Form. Selected Servers Folder and Server Folder addresses are saved in the registry so they are still there when you next start the program, making things a lot faster.

There is also the useful Start Time, End Time, Start and Stop buttons. These allow you to set a time after which the server should be running and a time after which it should stop. The screen shot shows these times set to 8am and midnight. The timer that checks these values (timer starts when you click the start button) ticks every 15 minutes so times are accurate to 15 minutes. This means the server could actually start at 8:15 rather than 8:00. Also, if you start the timer in the time gap that you have said you want the server running then the server will start immediately. I.e. with above settings if I start the timer at 1pm then the server will start immediately, stop at midnight then restart again 8am the next day. Finally, the times you set are also saved in the registry so when you next start up they are still there (but the program won't automatically start the timer!).

Then you have the Launch button, Stop button and Output box next to them. The Launch button allows you to start the server running, the stop button allows you to stop it running. The stop button also acts as an overriding stop button that will stop the server and also timer if the timer is running to prevent it from restarting. The output box shows all the text that normally displays in the command prompt output window. However, after sending commands to the server there is invariably an extra blank command sent so expect to see the server output (somewhere after the command) "Command not found. Use /help for the list of commands" - or words to that effect. I do not know why this is and if anyone knows and/or can fix it I would appreciate it.

Below this is the command buttons tab control and command input box. The tab control allows you to add command unlimited tabs with 12, equal sized, custom command buttons per tab. The default tab contains the standard command buttons that all MineCraft servers have (need), you can also see in the screen shot my own custom tab for commands for the MobArena plugin. To add a command button, select the tab you want to add it to (but not the default tab) then type in the command (in the command box) you want that button to be associated with. Finally, click the add command button. By typing in the exact same text to the command box you can remove the command button from your custom buttons. For commands that have additional arguments the syntax used is the same pseudo syntax used by MineCraft wiki documentation, namely:

"Command [Command Argument - Spaces allowed] [Command Argument] other command text"

e.g. "msg [Player Name] [Message]"

Spaces are allowed in the argument names. The argument names are what you are asked for when you click the button. See later explanation of the Command Arguments Form.

The tabs and buttons are saved in the registry when the program closes so don't worry about having to set them up every time you start the program. Also, the buttons are not dependent upon the server you use.

Finally, to send any command you like to the server simply type it in the command box and hit enter. This includes the stop command though I strongly recommend you do not issue the stop command through the command box as it will not be picked up by the rest of the management program.

So that's how to use the program, now onto how the code works.

Selecting servers folder and finding server folders within that:

The first thing you do is enter in the address of the folder that contains your servers' folders (or singular of this). You can also use the browse button that simply opens up a Folder Browser Dialog, pretty basic stuff. But once you've selected that the code then adds (to the Server Folder combo box) all the sub folders that contain runnable servers. You can of course select the exact folder again using a folder browser dialog. The code to discuss is how to detect if a folder is a server folder? The (core, stripped down) code looks as follows:

C#
//Get a list of top-level only directories that could be server folders - top-level only 
//prevents needlessly searching through the masses of sub-folders that servers have.
string[] SubDirectories = Directory.GetDirectories(ServersFolderAddressBox.Text, "*", 
                                                   SearchOption.TopDirectoryOnly);
//Loop through all of them
for (int i = 0; i < SubDirectories.Length; i++)
{
    //Check to see if they contain a valid server Jar file - uses previously discussed 
    //method for getting Jar file name but wrapped up in the ContainsServerJarFile method.
    if (MineCraftServer.ContainsServerJarFile(SubDirectories[i]))
    {
        //If it does, add it to the drop down list of possible server folders.
        ServerFolderAddressBox.Items.Add(SubDirectories[i]);
    }
}   

This uses the method I described in the MineCraftServer class to detect if a default MineCraft server jar file exists within the folder. If it does then the server can be used and so it adds it to the list. Classes used are in System.IO and are Directory and File classes that give quick and easy access to standard methods of file/directory handling.

Loading the server basic configuration: World and Port

To do this the code uses the ServerSettings class that has been described then loads the level-name and server-port settings into the relevant boxes. The code also does a search for folders ending in _nether. Why? Because under Bukkit MineCraft servers (including Tekkit which is built on Bukkit) it splits your world into three separate folders - one of which end sin _nether. This is, therefore, a quick and easy way of identifying worlds that are in the server's folder. It will not work for the standard MineCraft server so the list would be empty for those, however, world name is still automatically loaded from the config even if the world's folder is not found.

Other than that, most of the Main Form code is fairly boring apart from these last few items:

  1. Command Arguments Form
  2. Loading and saving of application settings using the registry

For the Command Arguments form I decided to create a form that would dynamically add text boxes and labels to its interface based on a list of names of required arguments passed to it. This provides maximum flexibility but still with lightweight code. I used the same form for adding tab pages and command button since it provided an easy way of getting user input about things such as new tab page name and new command button text.

Finally, loading and saving application settings from/to the registry. I used the registry key/sub-key structure to save tabs and there command buttons in the following format:

App Registry Key (Folder)
- Tab Page Sub Key (Folder, Name= "CommandPage_" + Tab page name)
- Command Button Key:
Key = Command + Button Number
Value = Command + "¬" + Button Text

The use of "CommandPage_" allows me to identify sub-keys that are tab pages, rather than assuming all sub keys are tab pages - this allows for more extensibility in future versions.

The code is the following for loading then saving:

C#
//Get all the sub keys then filter by whether they start with "CommandPage_"
List<string> CommandPagesSubKeyNames = Application.UserAppDataRegistry.GetSubKeyNames()
                                        .Where(x => x.Split('_')[0] == "CommandPage").ToList();
//For each of the sub keys identified as being a tab page
foreach (string APageSubKeyName in CommandPagesSubKeyNames)
{
    //Get the page name
    string PageName = "";
    //Split the name by underscore
    string[] KeyParts = APageSubKeyName.Split('_');
    //Get all the parts apart from the fist one (replacing underscores) - the name may have
    //originally contained underscores
    for(int i = 1; i < KeyParts.Length; i++)
    {
        PageName += KeyParts[i] + "_";
    }
    //Remove the final underscore
    PageName = PageName.Remove(PageName.Length - 1);
    //Create the new tab page
    TabPage NewPage = new TabPage(PageName);
    //Add it to the tab control
    CommandsTabControl.TabPages.Add(NewPage);
    //Get the pages registry sub key
    Microsoft.Win32.RegistryKey CommandPageSubKey = Application.UserAppDataRegistry.OpenSubKey(APageSubKeyName);
    //Get all the sub keys - which will be command buttons.
    string[] KeyNames = CommandPageSubKey.GetValueNames();
    //Loop through all of them - this could do with a check for max buttons allowed.
    for (int i = 0; i < KeyNames.Length; i++)
    {
        Button NewCommandButton = new Button();
        //Boring code for setting up button position goes here.

        //Get the value of the sub key
        string Val =  (string)CommandPageSubKey.GetValue(KeyNames[i]);
        //Command button text is the second bit
        NewCommandButton.Text = Val.Split('¬')[1];
        //The command itself is the second bit
        Commands.Add(Val.Split('¬')[0], NewCommandButton);
        //Add the command button to the tab page
        NewPage.Controls.Add(NewCommandButton);  
    }
} 
//Get all the existing tab page sub keys
List<string> CommandPagesSubKeyNames = Application.UserAppDataRegistry.GetSubKeyNames().Where(
                                               x => x.Split('_')[0] == "CommandPage").ToList();
//And loop through to delete them - this is easiest way of clearing out for a new save.
foreach (string APageSubKeyName in CommandPagesSubKeyNames)
{
    Application.UserAppDataRegistry.DeleteSubKeyTree(APageSubKeyName, false);
}
//Now loop through all the command button tab pages adding in the relevant keys.
foreach(TabPage ACommandPage in CommandsTabControl.TabPages)
{
    //If this isn't the default tab page
    if (ACommandPage.Text != "Default")
    {
        //Create a new key for this page
        Microsoft.Win32.RegistryKey ThisPageKey = Application.UserAppDataRegistry.CreateSubKey(
                                                  "CommandPage_" + ACommandPage.Text);
        //Store the button number
        int i = 0;
        //Loop through all the buttons
        foreach (Button ACommandButton in ACommandPage.Controls)
        {
            //Get the command for this button
            string ACommand = (from Comds in Commands
                                where (Comds.Value == ACommandButton)
                                select Comds.Key).First();
            //Add a new key for this button is above described format
            ThisPageKey.SetValue("CommandButton" + (i++).ToString(), 
                                 ACommand + "¬" + ACommandButton.Text);
        }
    }
}  

Compatibility with MineCraft, Bukkit and Tekkit

The new code offers (I hope) full compatibility with vanilla Minecraft server, Bukkit and Tekkit servers. If you have any problems or notice and features not working as they should, please let me know what isn’t working and what version of minecraft, Bukkit or Tekkit you are using.

Mod Installer

The mod installer form uses the zip basis of Jar files to convert them, install mods, and then convert back to Jar files. The mod installer uses the latest version of the .Net framework (4.5) which is still in Beta because it contains the new System.IO.Compression which specifically provides classes for handling zip archives. The code takes the Jar file, changes the extension to .zip, opens it, copies in the files from your selected folders to the desired location in the zip, overwriting any existing files, and then finally changes the extension back to .jar. This method works well for most mods, however, there are some issues with it. Jar and Zip do have some minor differences but as yet I can’t tell what they are. This means that a few popular mods (in particular ModLoader or ModLoaderMP) do not install properly. They result in a large and fatal error which, in essence, says that ModLoader can’t read the jar file properly. This error occurs with a lot of zip handling software such as 7Zip and ZipArchiver (though it does not occur consistently) yet WinRAR does not seem to cause this issue. I recommend therefore, that until I fix this issue you hold onto your copy of WinRAR.

Client Modding

The Mod Installer form is also capable of modding the client jar file. To do this simple select the “%appdata%\.minecraft” folder as the Servers Folder Address value then select “bin” as the server folder. Use the mod installer as you would normally and it will install mods in the minecraft.jar file.

C#
private void InstallButton_Click(object sender, EventArgs e)
{
    //Set total progress to zero
    TotalProgressBar.Value = 0;
    
    //Disable the form so that the user can't change stuff while we are installing mods
    this.Enabled = false;
    //Create FileInfo for the server's JAR file
    FileInfo FileInf = new FileInfo(ServerPath);
    //Create a string for the new destination of the file - the only change here is changing the file to a .zip extension.
    //I have not tried it without this change, it might be worth trying at some point.
    string DestZipFileName = FileInf.FullName.Replace(FileInf.Extension, ".zip");
    //Miove the file to the new file name, basically just changes the extension of the file.
    File.Move(ServerPath, DestZipFileName);
    //Open the ZipArchive (class is from System.IO.Compression namepsace.)
    using (ZipArchive ServerArchive = ZipFile.Open(DestZipFileName, ZipArchiveMode.Update))
    {
        //Set the maximum value of the TotalProgressBar to the number of mods we are installing
        TotalProgressBar.Maximum = ModsListBox.Items.Count;
        //For every mod listed, work through them in order.
        for(int i = 0; i < ModsListBox.Items.Count; i++)
        {                    
            //Get the next mod
            ListViewItem Item = ModsListBox.Items[i];
            //Update UI to show we are installing this mod
            TotalProgressBar.Value++;
            //Set the current progress through this mod to zero.
            CurrentProgressBar.Value = 0;
            
            //get all the files in the mod's directory and sub directories
            FileInfo[] Files = new DirectoryInfo(Item.Text).GetFiles("*.*", SearchOption.AllDirectories);
            //Set the maximum progress to how many files there are in this mod.
            CurrentProgressBar.Maximum = Files.Length;
            //Loop through all the mod's files
            foreach (FileInfo file in Files)
            {
                //Update UI to show progress
                CurrentProgressBar.Value++;
                //Get the name of the entry that this file will be in the Zip file. 
                //If the destination sub folder is blank, then don't add a leading backslash
                //Use the files full path so as to keep the direcotry structure in the zip file 
                //but remove the spurious back slahes and the leading (root) folder Uri.
                string NewEntryName = (!string.IsNullOrWhiteSpace(Item.SubItems[1].Text) ? 
                                       Item.SubItems[1].Text + "/" : "") + 
                                       file.FullName.Replace(Item.Text + "\\ " , "");
                //Replace back slahes with forward slahes - format of entry names
                NewEntryName = NewEntryName.Replace("\\ ", "/");
                //Get any existing file in the zip
                ZipArchiveEntry fileInZip = (from f in ServerArchive.Entries
                                             where f.FullName == NewEntryName
                                             select f).FirstOrDefault();
                //If there is an existing file (entry), delete it
                if (fileInZip != null)
                {
                    fileInZip.Delete();
                }
                //Add our new entry using fast compression.
                ServerArchive.CreateEntryFromFile(file.FullName, NewEntryName, CompressionLevel.Fastest);
            }
        }
        //If user wishes to delete the Meta-Inf folder, do so as below.
        if (DeleteMatInfCheckBox.Checked)
        {
            //Get all entries in the Meta-Inf folder
            List<ZipArchiveEntry> MetaInfFiles = (from f in ServerArchive.Entries
                                                  where f.FullName.Contains("META-INF")
                                                  select f).ToList();
            //And delete them
            MetaInfFiles.ForEach(x => x.Delete());
            //Deleting all entries remove the Meta-Inf "folder"
        }
    }
    //Change the extension back to .jar
    File.Move(DestZipFileName, ServerPath);
}


Editor

The Editor Form allows you currently only to view the items of a player’s inventory and their x, y, z location. It does not actually allow editing. I have thrown it in to preview what the future form will be like. It uses the Substrate C# library (which I recommend) for reading (and later writing) from/to the player data (.dat) files. By default it lists a minimum of 36 items but will list more, if for instance you are using the Hack/Mine mod which adds extra inventory spaces.

New UI

The latest version introduces some new UI features including:

  • Message log box
  • Connected players box
  • All players box
  • Associated buttons

The message log box primarily outputs the messages that players send in game or from the console. However, it also lists “giving” and “issued command” messages – when the users tried to give themselves items or issues server commands. These types of messages will also cause the task bar tray icon to flash until you view the program – it alerts you to potential cheating. I will soon release a new version that will allow you to turn this off. Final couple of messages listed are when users join or leave. All messages are formatted with just the time then username the message/information.

The connected player box gets updated when a join or leave message is detected. It lists all the players currently connected and has next to it various helpful command buttons including Kick, Ban and Message.

The all players box lists all players and is loaded from the “players” data folder when the server starts. It is also updated when a new player joins while the server is running. It comes with two command buttons, Pardon and Edit. The Edit button opens the Edit Form, though editing is not possible at the moment, only viewing of items/location.

Loading worlds

New in this version is loading of worlds based on folder contents rather than name of the folder. The code looks for “level.dat” files then uses their directory name as the world name. To avoid issues with Bukkit (or Tekkit) the code also checks if the directory name contains “_nether” or “_the_end" to stop it listing parts of worlds.

Stop Button Improvement – stops timer too

Improvements to the stop button code means that it will now also stop the timer as well as the server, previously it only stopped the server and, if the timer was still running, the server would be restarted within 15 minutes.

Updates

  1. Application now uses .Net Framework 4.5 - You will need to install this for full version it to work. May only come with Visual Studio 11 Beta - I'm not sure...
  2. Searching for worlds based on folder contents not name
  3. Mod Installer form - allows installation of mods from folders (i.e. unzip your mod's zip files) then select destination folder and go! It will even delete the Meta-Inf folder if you want it to. Modding also works on the client. Just select the server folder as the client's bin folder. This feature is what makes use of of the latest .Net framework version (4.5). It uses the improved System.IO.Compression namespace to do zip handling i.e. jar file handling.
  4. Improvements to UI such as
    : Addition of a Message Log Box which lists messages in friendly format including:
    - Messages from players and console
    - "Giving" messages - useful for detecting when people are cheating by giving themselves stuff : Causes tray icon to flash until main window is viewed.
    - Connect/Disconnect messages
    - "Command" messages - Useful for detecting cheating players who issue server commands : Causes tray icon to flash until main window is viewed.
    : Addition of Connected Players list box and associated command buttons
    : Addition of All Players list box (loaded from data files) and some useful command buttons
  5. Edit Player Form - Makes use of Substrate MineCraft C# library - can be found on Google Code here. This currently doesn't allow editing of user items but will list Item IDs and other info if available. Will also tell you their x, y, z coordinates.
  6. Minor bug fixes in the UI and function

Points of Interest

The main weird and slightly frustrating thing learnt was the MineCraft (or Java) quirk that requires the "-nojline" argument to fix.
Also interesting is that 32-bit machines can only allocate up to 4G of physical memory at any given time, hence why the program needs to be compiled to 64-bit. To get a 32-bit version working I would need to experiment with how little memory MineCraft servers actually need/will accept.

Useful Links

History

7/4/12 - Initial release
13/5/12 - First update. Main new features: Mod Installer, Message Log Box, Edit Player Form

License

This article, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)