Revisiting the existing Dragon’s Spine game server, making sure we can easily build and run it.
From .NET Framework 2.0 to Mono #
First things first, I wanted to start building the game server in an open-source environment.
Given the age of the code,
it looks like I’ll have more luck with
Mono and
xbuild
than the more recent
dotnet products.
So pull down the code on a current-ish Ubuntu
install (the current winner in the category of can-I-find-help-on-the-web),
a quick sudo apt install mono-complete mono-dbg mono-xbuild
,
and awaaay we go.
The Usual Suspects #
$ XBUILD_EMIT_SOLUTION=yes xbuild /p:Configuration=Debug DragonsSpine.sln
...
CSC: error CS2001: Source file './WindowForms/CharacterEditor.designer.cs' could not be found.
...
$ ls ./WindowForms/CharacterEditor.*
./WindowForms/CharacterEditor.cs ./WindowForms/CharacterEditor.resx
./WindowForms/CharacterEditor.Designer.cs
As expected when going from a case-insensitive OS to a case-sensitive OS, case mismatches which
were silently tolerated before will now cause issues. In this case, we have a .Designer.cs
file
referred to by .designer.cs
in the project file. Easy enough to correct in the file (even though
I’m not using this Windows Forms code currently) and move on.
$ XBUILD_EMIT_SOLUTION=yes xbuild /p:Configuration=Debug DragonsSpine.sln
...
/usr/lib/mono/xbuild/14.0/bin/Microsoft.Common.targets: warning : Reference 'GameLib,
Version=0.2.2039.20837, Culture=neutral' not resolved
...
For searchpath bin/Debug/
Considered './bin/Debug/GameLib.dll' as a file, but the file does not exist
...
/usr/lib/mono/xbuild/14.0/bin/Microsoft.Common.targets: warning : Reference 'Net,
Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL' not resolved
...
For searchpath bin/Debug/
Considered './bin/Debug/Net.dll' as a file, but the file does not exist
...
GameSystems/Network/Clients/Protocol/ProtoClientIO.cs(7,7): error CS0246: The type or namespace
name 'Net' could not be found (are you missing a using directive or an assembly reference?)
World/Map.cs(6,7): error CS0246: The type or namespace name 'GameLib' could not be found (are you
missing a using directive or an assembly reference?)
...
$ ls ./Dlls/
DevAge.Core.dll DevAge.SourceGrid3.dll DevAge.Windows.Forms.dll Net.dll
DevAge.Data.dll DevAge.SourceGrid3.Extensions.dll DevAge.WindowsPlatform.dll
DevAge.Drawing.dll DevAge.Utilities.dll GameLib.dll
Another common porting issue, search paths. I initially considered just copying the two DLLs
into the output directory (and this worked when I tried it). But some digging sent me to an old
stackoverflow question
that led me to include a HintPath
instead.
|
|
$ XBUILD_EMIT_SOLUTION=yes xbuild /p:Configuration=Debug DragonsSpine.sln
...
Build succeeded.
...
Cleanup #
But are we throwing any warnings we might want to look into?
./DragonsSpine.sln: warning : Don't know how to handle GlobalSection SourceCodeControl, Ignoring.
|
|
A reference to a project in Microsoft Visual SourceSafe? I think I can safely remove that.
DragonsSpineMain.cs(972,20): warning CS0219: The variable 'DeviceID' is assigned but its value is
never used
GameObjects/GamePlayer.cs(309,22): warning CS0169: The field 'GamePlayer.m_isCorpseCarried' is
never used
GameObjects/GamePlayer.cs(313,24): warning CS0414: The field 'GamePlayer.m_displayText' is
assigned but its value is never used
GameSystems/Groups/Group.cs(30,14): warning CS0414: The field 'Group.m_pauseToBuff' is
assigned but its value is never used
GameObjects/GamePlayer.cs(190,22): warning CS0414: The field 'GamePlayer.m_isAFK' is
assigned but its value is never used
GameSystems/Network/Clients/Protocol/ProtoClientIO.cs(14,22): warning CS0414: The field
'ProtoClientIO.running' is assigned but its value is never used
GameObjects/GamePlayer.cs(195,24): warning CS0414: The field 'GamePlayer.m_afkMessage' is
assigned but its value is never used
GameSystems/Mail/Mail.cs(17,14): warning CS0414: The field 'Mail.m_sent' is
assigned but its value is never used
GameObjects/GamePlayer.cs(197,22): warning CS0414: The field 'GamePlayer.m_wasInvisible' is
assigned but its value is never used
9 Warning(s)
0 Error(s)
Those all seem pretty innocuous, but let’s make a clean build. After commenting out all of those fields:
Build succeeded.
0 Warning(s)
0 Error(s)
Will it Run? #
$ mono --debug bin/Debug/DragSpinExp.exe
Unhandled Exception:
System.DllNotFoundException: kernel32 assembly:<unknown assembly> type:<unknown type> member:(null)
at (wrapper managed-to-native) DragonsSpine.WinConsole.GetConsoleWindow()
at DragonsSpine.WinConsole.Initialize () [0x00011] in ./GameSystems/Utilities/WinConsole.cs:283
at DragonsSpine.DragonsSpineMain.Main (System.String[] args) [0x00016] in ./DragonsSpineMain.cs:88
[ERROR] FATAL UNHANDLED EXCEPTION: System.DllNotFoundException: kernel32 assembly:<unknown assembly>
type:<unknown type> member:(null)
at (wrapper managed-to-native) DragonsSpine.WinConsole.GetConsoleWindow()
at DragonsSpine.WinConsole.Initialize () [0x00011] in ./GameSystems/Utilities/WinConsole.cs:283
at DragonsSpine.DragonsSpineMain.Main (System.String[] args) [0x00016] in ./DragonsSpineMain.cs:88
Ok, it crashes immediately. WinConsole
certainly sounds like something that might expect that it’s
running under Windows. Let’s see:
|
|
Since I’m intending to run the server in the background, what happens if I just comment out these
WinConsole
calls?
$ mono --debug bin/Debug/DragSpinExp.exe
5/30/2023 8:23:59 PM: {SystemGo} Drag Spin Exp 2.0.99.18
> 5/30/2023 8:23:59 PM: {SystemGo} Compiling scripts.
> 5/30/2023 8:23:59 PM: {SystemGo} Compiled!
> 5/30/2023 8:23:59 PM: {SystemGo} Added Facet: Agate
> 5/30/2023 8:23:59 PM: {SystemGo} Added Land: Beginner's Game to Facet: Agate
> 5/30/2023 8:23:59 PM: {SystemGo} Added Land: Advanced Game to Facet: Agate
> 5/30/2023 8:23:59 PM: {SystemGo} Added Land: Underworld to Facet: Agate
> 5/30/2023 8:23:59 PM: {SystemGo} Loaded Map: Island of Kesmai
> 5/30/2023 8:23:59 PM: {SystemGo} Loaded Map: Leng
> 5/30/2023 8:23:59 PM: {SystemGo} Loaded Map: Axe Glacier
> 5/30/2023 8:23:59 PM: {SystemGo} Loaded Map: Oakvael
> 5/30/2023 8:23:59 PM: {SystemGo} Loaded Map: Underkingdom
> 5/30/2023 8:23:59 PM: {SystemFailure} Land.FillLand() failed while loading map file for Island of Kesmai.
> 5/30/2023 8:23:59 PM: {SystemFailure} Land.FillLand() failed while loading map file for Leng.
> 5/30/2023 8:23:59 PM: {SystemFailure} Land.FillLand() failed while loading map file for Axe Glacier.
...
The Usual Suspects - Part Two #
|
|
Another common issue when porting code that touches the file system: path separators. Putting
slashes or backslashes directly into a path string will work only where that path separator is in
use. I could switch these to slashes, but it’s better to make it work anywhere. In this case we can
just use Path.DirectorySeparatorChar
, although there may be other options.
It also looks for the maps with a relative path, assuming the current working directory is the executable’s location. Let’s find the correct path no matter where we start, and enhance the log to make it easier to debug in future:
|
|
This also looks a little odd:
$ tree -d bin
bin
├── Debug
└── Debug\
├── Logs
│ └── 5_30_23
└── Scripts
Another errant backslash:
|
|
Also easily replaced with Path.DirectorySeparatorChar
.
$ mono --debug bin/Debug/DragSpinExp.exe
5/30/2023 11:17:58 PM: {SystemGo} Drag Spin Exp 2.0.99.18
> 5/30/2023 11:17:58 PM: {SystemGo} Compiling scripts.
> 5/30/2023 11:17:58 PM: {SystemGo} Compiled!
> 5/30/2023 11:17:58 PM: {SystemGo} Added Facet: Agate
> 5/30/2023 11:17:58 PM: {SystemGo} Added Land: Beginner's Game to Facet: Agate
> 5/30/2023 11:17:58 PM: {SystemGo} Added Land: Advanced Game to Facet: Agate
> 5/30/2023 11:17:58 PM: {SystemGo} Added Land: Underworld to Facet: Agate
> 5/30/2023 11:17:58 PM: {SystemGo} Loaded Map: Island of Kesmai
> 5/30/2023 11:17:58 PM: {SystemGo} Loaded Map: Leng
...
> 5/30/2023 11:18:07 PM: {SystemGo} Listening for connections on port 3000.
> Protocol Server listening to port 4000.
5/30/2023 11:18:07 PM: {SystemGo} Starting main game loop.
> 5/30/2023 11:23:07 PM: NPCs: [1755] | Players: [0] | CPU: [??%] | Rnd: [6]
...
A Oeuia
[]. A . . . .
[]. . . . . .
| . . . . . .
[]> --
. . . . . . .
. . . . . . .
. . ~~~~~~~~.
Oeuia: Fresh Balm of Gilead here!
->
R Hits : 40/40 Hits Taken : 0
L Experience : 1600
Stamina : 9
I’m sure more issues will come up, but this is working well enough to move forward.
Into a Container #
Database First #
Instead of reviving the laptop where I got Microsoft SQL Server running and happy on the network, let’s spin up a container. Luckily Microsoft maintains Microsoft SQL Server Ubuntu-based images, documentation for SQL Server on Linux 2017, and even some nice Docker examples.
Leveraging their examples, I threw together a Dockerfile
and supporting scripts to bring up my
databases:
|
|
Unlike the examples, I don’t want an image with no databases. Since importing the data takes
time (especially the production data), I want to do it when creating the image. So I run my
configure-db.sh
script while building the image; it starts up the server, imports the canned
data, and shuts it down again. I need a default password for the database server while I’m doing
this, so TempBadPassw0rd
it is.
|
|
|
|
Note that we force the password to be reset when bringing the server back up via the entrypoint. Accidentally leaving a running server with a default password that’s in a public repo is bad form.
Does it work?
$ SQLSERVR_SA_PASSWORD=StrongPassw0rd docker run -e 'ACCEPT_EULA=Y' -e SQLSERVR_SA_PASSWORD -p 1433:1433 --name sql --net drag-spin-exp -d
mssql-dragspinexp
a0f9cbd094e36685f5093335e92d9e8eefd2e13ebd8ab034f70662892a78c83f
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a0f9cbd094e3 mssql-dragspinexp "./entrypoint.sh" 17 seconds ago Up 16 seconds 0.0.0.0:1433->1433/tcp, :::1433->1433/tcp sql


> SELECT catalogID, itemID, identifiedName, shortDesc
FROM minimal.dbo.CatalogItem
WHERE name = 'ring'
catalogID|itemID|identifiedName|shortDesc|
---------+------+--------------+---------+
57| 13990|Recall Ring |a ring |
69| 13100|Gold Ring |a ring |
2 row(s) fetched.
> SELECT catalogID, itemID, identifiedName, shortDesc
FROM production.dbo.CatalogItem
WHERE name = 'ring'
catalogID|itemID|identifiedName |shortDesc|
---------+------+----------------------------------+---------+
41| 13250|Ring of Major Strength |a ring |
57| 13990|Recall Ring |a ring |
58| 13999|Symbol of the Order |a ring |
...
For now, one image with both databases (minimal for integration tests, production for running the game) seems to make sense; I can always split them later if it makes sense.
Game Server Next #
Now that we have the game server running under Mono, we can take advantage of
the official Mono Docker images. For now, we can use the same
base image to build and to run the game server, with a pretty simple Dockerfile
:
|
|
In the App.config
file, we refer to the database server simply as sql
, which will resolve to
the SQL Server container by that name if we put them on the same Docker network.
But wait, the other database details are also baked into the App.config
file, which would leave
them baked into the image and not able to be changed when it’s run:
|
|
So instead, let’s bake in some to-be-changed placeholder text:
|
|
And an entrypoint script which will use sed
to replace the placeholders with environment
variables (and complain and exit if they’re not set) before launching the server:
|
|
Then we just call that from the Dockerfile
:
|
|
Will it Run? - Part Two #

In a word, yes. (Animated GIF courtesy of Charm’s VHS.)
But those commands look pretty awkward. Certainly there’s an easier way? Tomorrow.
One More Thing #
If you’re curious what a user manual for an MMO launched in 1985 looked like, check it out. There had been scans online, but I did a quick OCR and conversion to Markdown so it’s easy to work with. The “Inhabitants of the Dungeon” section is missing from this particular copy, so I may try to find that somewhere else in future.