Sep 7, 2018

Speedy Network Debugging

A majority of my time programming is spent debugging existing code which requires frequently running the game to test to see if changes work. Optimizing the time it takes to get the game launched is usually not a problem, but when debugging network code it's a pain to get everything setup for testing and so I automated it. This code goes in the game before it starts running the update loop. The game can't run twice from the same location as it locks up trying to grab the same files at the same times so step one is to copy the game somewhere else.

private static void DirectoryCopy(string sourceDirectoryName, string destDirectoryName)
{
    DirectoryInfo sourceDirectory = new DirectoryInfo(sourceDirectoryName);
    if(!Directory.Exists(destDirectoryName))
    {
        Directory.CreateDirectory(destDirectoryName);
    }
    FileInfo[] files = sourceDirectory.GetFiles();
    foreach(FileInfo file in files)
    {
        string destinationPath = Path.Combine(destDirectoryName, file.Name);
        FileInfo existingDestinationFile = new FileInfo(destinationPath);
        if(!existingDestinationFile.Exists || file.LastWriteTime > existingDestinationFile.LastWriteTime)
        {
            Console.WriteLine("Copying " + file.Name + " to " + destinationPath);
            file.CopyTo(destinationPath, true);
        }
    }
    foreach(DirectoryInfo subdir in sourceDirectory.GetDirectories())
    {
        string subdirectoryDestinationName = Path.Combine(destDirectoryName, subdir.Name);
        DirectoryCopy(subdir.FullName, subdirectoryDestinationName);
    }
}

The above code has been optimised to only copy over files that have changed but it actually still runs a bit too slowly for my liking and so we can speed it up using Kernel32 file functions. Stuff like this makes me wonder why I even bother with C#.

const int INVALID_HANDLE_VALUE = -1;
const int FILE_ATTRIBUTE_DIRECTORY = 16;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal sealed class FILETIME
{
    public uint Low;
    public uint High;
    public Int64 ToInt64()
    {
        Int64 h = High;
        h = h << 32;
        return h + Low;
    }
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal sealed class FindData
{
    public int fileAttributes;
    public FILETIME CreationTime;
    public FILETIME LastAccessTime;
    public FILETIME LastWriteTime;
    public int FileSizeHigh;
    public int FileSizeLow;
    public int dwReserved0;
    public int dwReserved1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public String fileName;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
    public String alternateFileName;
}
[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
internal static extern IntPtr FindFirstFile(String fileName, [In, Out] FindData findFileData);
[DllImport("kernel32", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FindNextFile(IntPtr hFindFile, [In, Out] FindData lpFindFileData);
[DllImport("kernel32", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool FindClose(IntPtr hFindFile);
private static void FastDirectoryCopy(string sourceDirectoryName, string destDirectoryName)
{
    DirectoryInfo sourceDirectory = new DirectoryInfo(sourceDirectoryName);
    
    if(!Directory.Exists(destDirectoryName))
    {
        Directory.CreateDirectory(destDirectoryName);
    }
    List<string> subDirectories = new List<string>();
    IntPtr destFileHandle;
    FindData destFileData = new FindData();
    FindData sourceFileData = new FindData();
    IntPtr sourceFileHandle = FindFirstFile(sourceDirectoryName + "\\*", sourceFileData);
    bool fileFound = sourceFileHandle.ToInt32() != INVALID_HANDLE_VALUE;
    while(fileFound)
    {
        if((sourceFileData.fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0)
        {
            string destinationPath = Path.Combine(destDirectoryName, sourceFileData.fileName);
            destFileHandle = FindFirstFile(destinationPath, destFileData);
            if(destFileHandle.ToInt32() == INVALID_HANDLE_VALUE || sourceFileData.LastWriteTime.ToInt64() > destFileData.LastWriteTime.ToInt64())
            {
                Console.WriteLine("Copying " + sourceFileData.fileName + " to " + destinationPath);
                string sourcePath = Path.Combine(sourceDirectoryName, sourceFileData.fileName);
                File.Copy(sourcePath, destinationPath, true);
            }
            FindClose(destFileHandle);
        }
        else if(sourceFileData.fileName != "." && sourceFileData.fileName != "..")
        {
            subDirectories.Add(Path.Combine(sourceDirectoryName, sourceFileData.fileName));
        }
        fileFound = FindNextFile(sourceFileHandle, sourceFileData);
    }
    FindClose(sourceFileHandle);
    
    foreach(DirectoryInfo subdir in sourceDirectory.GetDirectories())
    {
        string subdirectoryDestinationName = Path.Combine(destDirectoryName, subdir.Name);
        FastDirectoryCopy(subdir.FullName, subdirectoryDestinationName);
    }
}

Then we write out a different debug info file for each instance of the game that will tell one to host while the other joins and also set the window positions.

string serverDebuggingJSONString = @"
    {
        ""Auto"":""HostAndStart1"",
        ""WindowX"":0,
        ""WindowY"":0,
    }"
;
string clientDebuggingJSONString = @"
    {
        ""Auto"":""Join"",
        ""WindowX"":900,
        ""WindowY"":300,
    }"
;
string ourSettings, otherSettings;
// attach is a variable that lets us easily swap between the debugger connecting to the client or the server
if(attach == AttachDebuggerTo.Server)
{
    ourSettings = serverDebuggingJSONString;
    otherSettings = clientDebuggingJSONString;
}
else
{
    ourSettings = clientDebuggingJSONString;
    otherSettings = serverDebuggingJSONString;
}
File.WriteAllText("debugSettings.txt", ourSettings);
File.WriteAllText(Path.Combine(destDir, "debugSettings.txt"), otherSettings);

Then we can launch the second process and this process will go to it's normal stating point for running the game.

var startInfo = new ProcessStartInfo()
{
    WorkingDirectory = destDir,
    FileName = Path.Combine(destDir, "KradensCrypt-Windows.exe")
};
Process.Start(startInfo);

The game loads and the menu code reads the debug info files from above and sets the different window positions and runs the network host and join and start process automatically so that a network game successfully starts. This has been invaluable in testing network code combined with a console command that lets me load up any room to test. There's even an item room you can load up to equip yourself and it feels to me a little bit like that one scene in the matrix :p




contact@hernblog.com