Home     Sign in    
Minecraft!
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! :-)

© 2001-2017 Simbey.com