4
\$\begingroup\$

I've been working on a game that I used to play as a kid. This game was disassembled then converted to C# by someone else and they hosted it on Github. I've forked it and started working on it some. One of the things I've been trying to convert over is how to read back the files that were saved during a save operation of the game.

Now, although I'm only asking about 1 specific field in this structure, having any tips on how to clean up my code would be much appreciated. After the first field the next 4 fields I have yet to convert into a meaningful structure so I've left them as large chunks of data.

Here is the structure I'm marshling the data into:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1, Size = 0x335D)]
public struct CurseSaveGame
{
    public byte GameArea;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x800)]
    public byte[] AreaPointer1;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x800)]
    public byte[] AreaPointer2;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x200)]
    public ushort[] SomeStructure;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x1E00)]
    public byte[] EclPointer;

    public sbyte MapPosX;
    public sbyte MapPosY;
    public byte MapDirection;
    public byte MapWallType;
    public byte MapWallRoof;

    public GameState LastGameState;
    public GameState GameState;

    public SetBlock SetBlock1;
    public SetBlock SetBlock2;
    public SetBlock SetBlock3;

    public PlayerLoadFiles Players;

    public byte[] ToByteArray() => Extensions.ToByteArray(this);
    public static CurseSaveGame Parse(byte[] array) => Extensions.MarshalAs<CurseSaveGame>(array);
}

Here are the structures that I've mapped over thus far in that structure

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1, Size = 4)]
public struct SetBlock
{
    public short BlockId;
    public short SetId;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1, Size = 0x149)]
public struct PlayerLoadFiles
{
    public byte NumberOfPlayersInParty;
    public CharacterLoadFile Player1;
    public CharacterLoadFile Player2;
    public CharacterLoadFile Player3;
    public CharacterLoadFile Player4;
    public CharacterLoadFile Player5;
    public CharacterLoadFile Player6;
    public CharacterLoadFile Player7;
    public CharacterLoadFile Player8;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1, Size = 0x29)]
public struct CharacterLoadFile
{
    private byte FileNameLength;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x08)]
    public string FileName;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)]
    private byte[] Houses;
}
public enum GameState : byte
{
    StartGameMenu = 0,
    Shop = 1,
    Camping = 2,
    WildernessMap = 3,
    DungeonMap = 4,
    Combat = 5,
    AfterCombat = 6,
    EndGame = 7
}

The part in question is the structure PlayerLoadFiles. I think that it would be best to have it more like:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1, Size = 0x149)]
public struct PlayerLoadFiles
{
    public byte NumberOfPlayersInParty;
    public CharacterLoadFile[] LoadFiles;
}

but when I do that (with different attributes attached to the PlayerLoadFiles field I keep getting the same error message when I Parse the array. Type 'CurseSaveGame' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed. Mentally I feel like this is the closest attribute for LoadFiles but again it fails:

[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CharacterLoadFile), SizeConst = 8)]

I suppose I can just keep the 8 structures as it works and I have the number of players. I suppose I could put a public getter on there with all 8 players as an array then use Linq's Take method to only take the correct number of players.

For testing purposes I did the following. I did a hex dump of the last 0x149 bytes and did a hex string to byte array, then marshaled that as the PlayerLoadFiles. I do it in Linqpad so it is fast and easy for me to visualize.

var loadData = new List<byte[]>
{
    @"06-08-43-48-52-44-41-54-41-31-00-00-00-0A-00-14-00 0C-00-AC-01-0D-0A-F3-00-08-01-C0-18-B2-21-C4-3C B8-02-20-00-00-00-00-00-18-08-43-48-52-44-41-54 41-32-46-00-46-00-40-00-96-13-FF-00-20-04-DA-3C 92-03-A5-47-01-00-20-00-04-00-00-00-00-00-18-00 28-00-08-43-48-52-44-41-54-41-33-3D-02-00-06-01 02-01-03-05-06-00-00-00-00-00-00-00-00-00-00-00 00-00-00-00-00-00-00-00-00-00-00-08-43-48-52-44 41-54-41-34-00-00-00-00-00-1E-06-01-06-02-4E-B6 54-77-00-84-54-77-00-66-54-77-00-06-59-65-73-20 4E-6F-19-00-08-43-48-52-44-41-54-41-35-09-09-0B 0B-0D-0D-0F-0F-11-11-13-13-15-15-17-17-19-19-00 00-00-00-00-1E-47-61-6D-65-20-6E-6F-74-08-43-48 52-44-41-54-41-36-51-75-69-74-20-61-6E-79-77-61 79-3F-20-20-4D-20-54-20-48-20-56-20-41-4E-C4-3D 31-12-A5-47-99-3D-B2-21-00-00-00-00-0F-00-0A-00 42-12-A5-47-C4-11-A5-47-4E-00-20-06-59-65-73-20 4E-6F-4E-00-1E-47-61-6D-65-20-6E-6F-74-20-73-61 76-65-64-2E-20-20-51-75-69-74-20-61-6E-79-77-61 79-3F-20-00-00-00-18-00-28-00-CE-3D-02-4E-F8-3E 47-06-82-38-0F-5E-0A-5E".ToByteArray(),
};
loadData.Select(MarshalAs<PlayerLoadFiles>).Dump();
public static T MarshalAs<T>(byte[] rawDataStructure) where T : struct
{
    var type = typeof(T);
    int size = Marshal.SizeOf(type);
    IntPtr ptr = Marshal.AllocHGlobal(size);
    Marshal.Copy(rawDataStructure, 0, ptr, size);
    T structure = (T)Marshal.PtrToStructure(ptr, type);
    Marshal.FreeHGlobal(ptr);

    return structure;
}
public static byte[] ToByteArray(this string hexString)
{
    hexString = hexString.ToUpper();
    hexString = System.Text.RegularExpressions.Regex.Replace(hexString, "[^0-9A-F.]", "");
    if (hexString.Length % 2 == 1)
        throw new Exception("The binary key cannot have an odd number of digits");

    byte[] arr = new byte[hexString.Length >> 1];

    for (int i = 0; i < hexString.Length >> 1; ++i)
    {
        arr[i] = (byte)((GetHexVal(hexString[i << 1]) << 4) + (GetHexVal(hexString[(i << 1) + 1])));
    }

    return arr;
}

private static int GetHexVal(char hex)
{
    int val = (int)hex;
    return val - (val < 58 ? 48 : 55);
}

Any feedback on how I can shrink this up some more and make it more readable would be great. Although I'm not new to byte level data this marshaling thing is sorta new to me like this.

Note: this is for the game Curse of the Azure bonds. You can see my fork of the code here on GitHub.

\$\endgroup\$
1
\$\begingroup\$

If there really always is 8 player objects, even in the cases where NumberOfPlayersInParty is less than 8, then you can use ByValArray:

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x08)]
public CharacterLoadFile[] Players;

But if there is only as many player object as NumberOfPlayersInParty indicates, then even your original solution is not correct and you will probably have to resort to deserializing the data at least partially manually.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.