Home     Sign in    
Articles
2/2013
Minecraft!
8/2011
Subversion!
8/2010
A summer of accomplishments!
6/2010
Spring Happenings
4/2010
My Little Helpers
Archived News
(1) Cancer cells programmed back to normal by US scientists
(2) Police clash with Michael Brown shooting protesters in Ferguson
(3) IBM Builds a Chip That Works Like Your Brain
(4) Newly Discovered Brain Cells Tell Stem Cells to Grow New Neurons
(5) Gross Domestic Freebie
Photo Archive
Browse by Folder
Browse by Calendar
Apps
Bubbles
JSON Parser
Neural Circuits
SIF Library

Simbey's Stuff The basic portal page  
Minecraft!
Subscribe to feed Updated 2/2/2013 8:07:00 PM by Simbey
The last time I posted something here, I wrote about Subversion.  Well, more than 600 check-ins (and a year and a half) later, the same repository that hosts the SimbeyServer and my customized version of Subversion now also hosts a project for managing a Minecraft server!

I've seen some other projects that do this.  The initial problem is that Minecraft's server wants to run as a console app.  It prints stuff through console output and reads administrative commands from console input.  That works great when you're hosting a LAN party and can run a private server from the same machine where you're also playing, but that model doesn't work so well on a headless server where usually no one's logged into an interactive desktop session.

The projects I've seen solving this problem have been stand-alone Win32 services.  And they've been written in C#.  So here's my native C++ solution to the problem.

The artwork is incomplete, but look at all those services!

Regrettably, I'm still using HTML to build the administrative side of the SimbeyServer, but it does work.  As you can see, Subversion is still hosted.  I've also put together a very basic skeleton project for IRC.  I'll have some more words on that project in a moment.  For now, let's talk about the why and how of hosting Minecraft with a C++ plug-in for the SimbeyServer!

Over this past Christmas and New Year's Eve time frame, I spent more time than I feel comfortable admitting playing on a public Minecraft server.  It was the end of the year, and I felt checked-out.  And I decided to try a public server.

The particular server I tried out was running Bukkit and the Factions mod, among several others.  I really liked the organization made possible by Factions, so, after the server admins decided to close the server, I decided to open my own and run Factions on it.

Welcome to Simbey's Minecraft Server!

In the screen shot above, if you're already familiar with Factions, you'll recognize the first line in the text history.  The public server I tried handed out a sword, pick, axe, and shovel to all new users, but I decided to be a lot more generous.  I hand out a hoe instead of a shovel, but I added wood, dirt, sand, cobblestone, and seeds.  I figure those resources will be enough to get someone started building a shelter with a wheat garden.  And then the Factions mod allows protection.

But that's not all that's going on in that screen shot!  When I initially setup the spawn point, Minecraft always moved new players above the roof!  Apparently that's the expected behavior.  But I wanted a different behavior.  So, the very first commands issued to the console are to explicitly teleport the new user and set that user's spawn point.

Okay, that's what's going on.  How does all of this happen?  It's pretty simple at the high level.  I have a file called HostedMCS.DLL.  The SimbeyServer loads this file during its startup and calls a series of DLL entry points.  Unlike SVN and IRC, the Minecraft server is an external process, so HostedMCS sets up a command line, creates some anonymous pipes, starts the process, and runs some threads.  You want an auto-save timer for the world too?  Okay, then we'll add a threadpool timer to that list.

HRESULT CHostedMCS::StartServerInternal (VOID)
{
    HRESULT hr;

    if(LockStartStop())
    {
        CHAR szJava[MAX_PATH];
        CHAR szXms[32], szXmx[32];
        CHAR szJarPath[MAX_PATH], szAbsoluteJar[MAX_PATH];
        CHAR szJarFile[MAX_PATH];
        CHAR szOptions[MAX_PATH];
        CHAR szCmdLine[ARRAYSIZE(szJava) + ARRAYSIZE(szXms) + ARRAYSIZE(szXmx) + ARRAYSIZE(szJarFile) + ARRAYSIZE(szOptions)];
        STARTUPINFOA si = {0};
        SECURITY_ATTRIBUTES secAttribs = {0};
        DWORD dwThreadId;

        secAttribs.nLength = sizeof(secAttribs);
        secAttribs.bInheritHandle = TRUE;

        ReloadStartItems();

        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "Java", szJava, ARRAYSIZE(szJava)));
        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "Xms", szXms, ARRAYSIZE(szXms)));
        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "Xmx", szXmx, ARRAYSIZE(szXmx)));
        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "JarPath", szJarPath, ARRAYSIZE(szJarPath)));
        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "JarFile", szJarFile, ARRAYSIZE(szJarFile)));
        Check(m_pHost->GetHostedProperty(m_pvCookie, NULL, "Options", szOptions, ARRAYSIZE(szOptions)));

        Check(m_pHost->ResolvePathAgainstServerRoot(szJarPath, TStrLenAssert(szJarPath), szAbsoluteJar, ARRAYSIZE(szAbsoluteJar)));

        Check(Formatting::TPrintF(szCmdLine, ARRAYSIZE(szCmdLine), NULL, "\"%hs\" -Xms%hs -Xmx%hs -jar %hs %hs", szJava, szXms, szXmx, szJarFile, szOptions));

        si.cb = sizeof(si);
        si.dwFlags = STARTF_USESTDHANDLES;

        CheckIfGetLastError(!CreatePipe(&si.hStdInput, &m_hInputPipe, &secAttribs, 0));
        CheckIfGetLastError(!CreatePipe(&m_hOutputPipe, &si.hStdOutput, &secAttribs, 4096));
        CheckIfGetLastError(!CreatePipe(&m_hErrorPipe, &si.hStdError, &secAttribs, 1024));

        CheckIfGetLastError(!SetHandleInformation(m_hInputPipe, HANDLE_FLAG_INHERIT, 0));
        CheckIfGetLastError(!SetHandleInformation(m_hOutputPipe, HANDLE_FLAG_INHERIT, 0));
        CheckIfGetLastError(!SetHandleInformation(m_hErrorPipe, HANDLE_FLAG_INHERIT, 0));

        CheckIfGetLastError(!CreateProcessA(szJava, szCmdLine, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW, NULL, szAbsoluteJar, &si, &m_piServer));

        m_hOutputReader = CreateThread(NULL, 0, _OutputReader, this, 0, &dwThreadId);
        CheckIfGetLastError(NULL == m_hOutputReader);

        m_hErrorReader = CreateThread(NULL, 0, _ErrorReader, this, 0, &dwThreadId);
        CheckIfGetLastError(NULL == m_hErrorReader);

        CheckIfGetLastError(!RegisterWaitForSingleObject(&m_hProcessWaiter, m_piServer.hProcess, &CHostedMCS::_ProcessWaiter, this, INFINITE, WT_EXECUTEDEFAULT | WT_EXECUTEONLYONCE));

        // We're intentionally reusing szJava for writing the process ID back to XML.
        Check(Formatting::TUInt32ToAsc(m_piServer.dwProcessId, szJava, ARRAYSIZE(szJava), 10, NULL));
        Check(m_pHost->SetHostedProperty(m_pvCookie, NULL, c_szProcessID, szJava));

        hr = m_pHost->GetHostedProperty(m_pvCookie, NULL, "AutoSaveTimer", szOptions, ARRAYSIZE(szOptions));
        if(SUCCEEDED(hr))
        {
            // The "AutoSaveTimer" value is stored in minutes.  Multiply by 60000 to get milliseconds.
            UINT msAutoSave = Formatting::TAscToUInt32(szOptions) * 60000;
            if(0 < msAutoSave)
                m_AutoSaveTimer.Start(msAutoSave, msAutoSave, this, &CHostedMCS::OnAutoSave);
        }

        EnterCriticalSection(&m_cs);
        m_cAcceptingCommands++;
        LeaveCriticalSection(&m_cs);

    Cleanup:
        if(FAILED(hr))
        {
            SafeCloseHandle(m_hOutputReader);
            SafeCloseHandle(m_hErrorReader);
        }

        SafeCloseHandle(si.hStdInput);
        SafeCloseHandle(si.hStdOutput);
        SafeCloseHandle(si.hStdError);

        UnlockStartStop();
    }
    else
        hr = E_ACCESSDENIED;

    return hr;
}

Coding up all of this was an adventure in itself!  A big part of the trick was discovering the "-nojline" command line option.  Without that option, the Minecraft server wouldn't read the commands I wrote to it via the redirected input pipe!  With that working, I still had to read the output from Minecraft.  Options are limited with anonymous pipes, but the following has been working well.

VOID CHostedMCS::PipeReader (HANDLE hPipe)
{
    TStackRef<IPresenceChannel> srChannel;
    CHAR szLine[256];
    CHAR ch;
    DWORD cbRead;
    INT nLinePtr = 0;

    EnterCriticalSection(&m_cs);
    srChannel = m_pChannel;
    LeaveCriticalSection(&m_cs);

    Assert(NULL != srChannel);

    while(ReadFile(hPipe, &ch, sizeof(ch), &cbRead, NULL))
    {
        if('\r' != ch)
        {
            if('\n' == ch)
            {
                szLine[nLinePtr] = '\0';

                FILETIME ft;
                GetSystemTimeAsFileTime(&ft);

                EnterCriticalSection(&m_cs);
                if(SUCCEEDED(m_strCache[m_nCachePtr].Assign(nLinePtr, szLine)))
                {
                    CopyMemory(m_ftCache + m_nCachePtr, &ft, sizeof(FILETIME));
                    if(++m_nCachePtr == ARRAYSIZE(m_strCache))
                        m_nCachePtr = 0;
                }
                LeaveCriticalSection(&m_cs);

                srChannel->ChannelMessage(m_pNick, &ft, szLine, nLinePtr);

                ProcessServerMessage(szLine);

                nLinePtr = 0;
            }
            else if(nLinePtr < ARRAYSIZE(szLine) - 1)
                szLine[nLinePtr++] = ch;
        }
    }
}

I keep a cache of text from the server for replaying later.  But where would it be replayed, you ask?  And what's with that ChannelMessage() call?  This is where things tie back into IRC!

Eventually I will write an article specifically about the SimbeyServer.  It deserves one!  For now, what you'll want to know is that the SimbeyServer contains a series of objects collectively known as Presence.  My grand plan has been to implement IRC around the SimbeyServer's Presence objects.  All the users, permissions, channels, and invites are handled by Presence, leaving just the protocol specific stuff (like command parsing and response formatting) to the IRC implementation.  So, if Presence is a component of the SimbeyServer, accessible to all plug-ins, then that means...

The HostedMCS plug-in literally registers itself on startup as a Presence user!  It also creates a Presence channel and joins it!  When IRC users join this channel, they then receive all the cached output from Minecraft sent just to them through that channel.  Private messages sent to HostedMCS are passed through to Minecraft as console commands.  And that's how HostedMCS is administrated!

All the initial options for getting HostedMCS running come out of XML.  No setting that someone might want to adjust is hard coded.  The XML even tracks when users first registered (and that's how we know whether to hand out free stuff).

<services>
    <service module="C:\Program Files\SimbeyServer\Services\MCS\HostedMCS.dll" name="Minecraft">
        <property value="#SimbeyServerMCS" name="PresenceChannel"/>
        <property value="on" name="AutoStart"/>
        <property value="C:\Program Files\Java\jre7\bin\java.exe" name="Java"/>
        <property value="1024M" name="Xms"/>
        <property value="1024M" name="Xmx"/>
        <property value="..\Minecraft" name="JarPath"/>
        <property value="craftbukkit-1.4.6-R0.3.jar" name="JarFile"/>
        <property value="-nojline -o true" name="Options"/>
        <property value="Please welcome %hs to the server!" name="WelcomePublicMessage"/>
        <property value="Use your starting inventory wisely and clean up after explosions!" name="WelcomePrivateMessage"/>
        <property value="2189, 64, -5739" name="TeleportTo"/>
        <property value="5" name="AutoSaveTimer"/>
        <startitems>
            <sword>
                <property value="1" name="amount"/>
                <property value="272" name="type"/>
            </sword>
            <pick>
                <property value="1" name="amount"/>
                <property value="274" name="type"/>
            </pick>
            <axe>
                <property value="1" name="amount"/>
                <property value="275" name="type"/>
            </axe>
            <hoe>
                <property value="1" name="amount"/>
                <property value="291" name="type"/>
            </hoe>
            <planks>
                <property value="64" name="amount"/>
                <property value="2" name="stacks"/>
                <property value="5" name="type"/>
            </planks>
            <dirt>
                <property value="64" name="amount"/>
                <property value="3" name="type"/>
            </dirt>
            <cobblestone>
                <property value="32" name="amount"/>
                <property value="4" name="type"/>
            </cobblestone>
            <sand>
                <property value="64" name="amount"/>
                <property value="12" name="type"/>
            </sand>
            <seeds>
                <property value="32" name="amount"/>
                <property value="295" name="type"/>
            </seeds>
        </startitems>
        <registered>
            <simbey>
                <property value="2013-01-15 04:37:52" name="time"/>
            </simbey>
        </registered>
        <property value="15628" name="ProcessID"/>
    </service>
    <service module="C:\Program Files\SimbeyServer\Services\SVN\HostedSVN.dll" name="Subversion">
        <property value="C:\inetpub\simbeyserver\svn" name="Root"/>
        <property value="on" name="IPv4"/>
        <property value="off" name="IPv6"/>
    </service>
</services>
I hope you enjoyed reading this!  The project was definitely a lot of fun to build!  And if anyone from Mojang should happen upon this article, I'd love to discuss employment opportunities! :-)

Subversion!
Subscribe to feed Updated 8/4/2011 by Simbey
I finally have something I'd like to share with the Internet!  So, listen up, Internet!  After using Subversion (SVN) for over a year at BSQUARE, I've decided I really like it!  In fact, I like it so much that I integrated it into my server!

SVN is a software versioning and revision control system, but you can read all the details at Wikipedia.  In a nut shell, SVN stores the history of documents as they are changed over time.  This is great for software projects where source files are constantly being changed.  Versioning and revision control make it easy to see how the software is evolving over time.

Out of the box, there are two ways to run SVN on Windows.  It can be run over HTTP through the Apache web server, or it can be run through SVN's native networking protocol on port 3690 from a module called SVNServe.exe.  It was easy for me to pick the native protocol, but I didn't want to run an isolated executable.

So here's where it gets interesting.  Since SVN is open source, I'm not stuck choosing between Apache or SVNServe.  It took some time, but eventually I had the SVN sources built.  I found all the prebuilt header files I needed on various websites, so I never had to run the actual SVN build system, which uses Python or something awful like that.

Finally, by July 21st, I had SVNServe.exe built and running, and I began checking-in changes to my new repository!  The solution wasn't ready for deployment, however.  SVNServe.exe needed to be a DLL.  Why, you ask?  Simple.  SVN usually reads user names and passwords from a text file, and this just wasn't going to work for me.  My server already manages user names and passwords, so why not share them?

By July 24th, both the SVN sources and the SimbeyServer sources were hosted in SVN!  SVNServe.exe had been converted to HostedSVN.dll, hosted within the SimbeyServer's process.  When a client authenticates, SVN asks the SimbeyServer, through an in-proc interface, to verify users.  The SimbeyServer hosts its own source code!

Next problem...  SVN uses a cross-platform compatible library called Apache Portable Runtime (APR).  APR's actually not that bad, especially when compared to Nokia's Qt, which is a train wreck.  No one should use Qt.  EVER.  Know what uses Qt?  Amazon's desktop Kindle app.

SVNServe.exe, now HostedSVN.dll, has a loop that listens for incoming connections and creates a thread for each connection.  The method for exiting the loop involved closing the listener socket from another thread while the main loop was blocked within APR's apr_socket_accept() call.  Not exactly elegant.

The SimbeyServer already has a thread that manages all the listener sockets using the WSAEventSelect() model.  By August 2nd, I had eliminated the extra thread within HostedSVN.dll that was listening for connections, and the listener was being managed by the SimbeyServer's listener thread.  When a client connects to port 3690, the SimbeyServer passes the socket to HostedSVN.dll, and APR's apr_os_sock_make() API is used to wrap the socket for APR compatibility.  And the rest of the system continues to work as before!

And that's my story!  SVN is fully functional and, thanks to TortoiseSVN, is user-friendly!  So where do I go from here?  As crazy as it sounds, I may go grab K-9's sources and add them to my repository.  Once I have a little extra money, I might hire out some work.  With SVN, collaborative software development becomes very easy!  And there is so much I want to do!

© 2001-2015 Simbey.com