public static SavedD20Action Load(BinaryReader reader)
    {
        var result = new SavedD20Action();

        result.Type  = (D20ActionType)reader.ReadInt32(); // TODO: Think about action type
        result.Data  = reader.ReadInt32();
        result.Flags = (D20CAF)reader.ReadInt32();
        reader.ReadInt32(); // Padding
        reader.ReadInt64(); // Performer handle is transient
        reader.ReadInt64(); // Target handle is transient
        result.TargetLocation   = reader.ReadLocationAndOffsets();
        result.DistanceTraveled = reader.ReadSingle();
        result.RadialMenuArg    = reader.ReadInt32();
        result.RollHistoryId0   = reader.ReadInt32();
        result.RollHistoryId1   = reader.ReadInt32();
        result.RollHistoryId2   = reader.ReadInt32();
        result.SpellData        = ReadSpellData(reader);
        result.SpellId          = reader.ReadInt32();
        result.AnimActionId     = reader.ReadInt32();
        reader.ReadInt32(); // Path pointer is transient

        // ToEE saves the actual object id's after the struct it just dumped (which contains transient data)
        result.Performer = reader.ReadObjectId();
        result.Target    = reader.ReadObjectId();
        return(result);
    }
    public static SavedHotkeys Load(BinaryReader reader)
    {
        var hotkeys = new SavedHotkeys();

        for (var keyIndex = reader.ReadInt32(); keyIndex != -1; keyIndex = reader.ReadInt32())
        {
            var hotkey = new SavedHotkey();

            // Originally it just read the radial menu entry, but that contains so much
            // stale data it's not even funny.
            // The hotkey system will search the entire radial menu and compare each entry
            // against the following fields found in the hotkey system's copied radial menu entry:
            // - D20 action type
            // - D20 action data
            // - D20 spell data, spell enum original
            // - The upper 16 bit of the metamagic data
            // - Text Hash for certain action types
            reader.ReadInt32();                   // Stale text pointer
            reader.ReadInt32();                   // Stale text2 pointer
            hotkey.TextHash = reader.ReadInt32(); // Text elfhash
            reader.ReadInt32();                   // Padding
            reader.ReadInt32();                   // Radial menu entry type
            reader.ReadInt32();                   // min arg
            reader.ReadInt32();                   // max arg
            reader.ReadInt32();                   // Stale actual arg pointer
            var hotkeyActionType = reader.ReadInt32();
            hotkey.ActionData = reader.ReadInt32();
            reader.ReadInt32(); // Action CAF
            hotkey.SpellData = SavedD20Action.ReadSpellData(reader);
            reader.ReadInt32(); // Dispatcher key
            reader.ReadInt32(); // Callback pointer
            reader.ReadInt32(); // Flags
            reader.ReadInt32(); // Help text hash
            reader.ReadInt32(); // Spell Id

            hotkey.Text = reader.ReadFixedString(128);

            if (keyIndex >= AssignableKeys.Length)
            {
                throw new CorruptSaveException($"Hotkey table contains key which is outside range: {keyIndex}");
            }

            hotkey.Key = AssignableKeys[keyIndex];

            if (hotkeyActionType == -2)
            {
                continue; // Unassigned
            }

            hotkey.ActionType = (D20ActionType)hotkeyActionType;

            if (!hotkeys.Hotkeys.TryAdd(hotkey.Key, hotkey))
            {
                throw new CorruptSaveException($"Duplicate assignment to key {hotkey.Key}");
            }
        }

        return(hotkeys);
    }
    public void Save(BinaryWriter writer)
    {
        foreach (var(key, hotkey) in Hotkeys)
        {
            var keyIndex = Array.IndexOf(AssignableKeys, key);
            if (keyIndex == -1)
            {
                Logger.Error("Cannot save hotkey assigned to {0} because the save format doesn't support it.", key);
            }

            // Originally it just read the radial menu entry, but that contains so much
            // stale data it's not even funny.
            // The hotkey system will search the entire radial menu and compare each entry
            // against the following fields found in the hotkey system's copied radial menu entry:
            // - D20 action type
            // - D20 action data
            // - D20 spell data, spell enum original
            // - The upper 16 bit of the metamagic data
            // - Text Hash for certain action types
            writer.WriteInt32(0);               // Stale text pointer
            writer.WriteInt32(0);               // Stale text2 pointer
            writer.WriteInt32(hotkey.TextHash); // Text elfhash
            writer.WriteInt32(0);               // Padding
            writer.WriteInt32(0);               // Radial menu entry type
            writer.WriteInt32(0);               // min arg
            writer.WriteInt32(0);               // max arg
            writer.WriteInt32(0);               // Stale actual arg pointer
            writer.WriteInt32((int)hotkey.ActionType);
            writer.WriteInt32(hotkey.ActionData);
            writer.WriteInt32(0); // Action CAF
            SavedD20Action.WriteSpellData(writer, hotkey.SpellData);
            writer.WriteInt32(0); // Dispatcher key
            writer.WriteInt32(0); // Callback pointer
            writer.WriteInt32(0); // Flags
            writer.WriteInt32(0); // Help text hash
            writer.WriteInt32(0); // Spell Id

            writer.WriteFixedString(128, hotkey.Text);
        }

        writer.WriteInt32(-1); // Terminator
    }
    internal static D20Action LoadAction(SavedD20Action savedAction)
    {
        var action = new D20Action
        {
            d20ActType          = savedAction.Type,
            data1               = savedAction.Data,
            d20Caf              = savedAction.Flags,
            destLoc             = savedAction.TargetLocation,
            distTraversed       = savedAction.DistanceTraveled,
            radialMenuActualArg = savedAction.RadialMenuArg,
            rollHistId0         = savedAction.RollHistoryId0,
            rollHistId1         = savedAction.RollHistoryId1,
            rollHistId2         = savedAction.RollHistoryId2,
            d20SpellData        = savedAction.SpellData,
            spellId             = savedAction.SpellId,
            animID              = savedAction.AnimActionId
        };

        if (!savedAction.Performer.IsNull)
        {
            action.d20APerformer = GameSystems.Object.GetObject(savedAction.Performer);
            if (action.d20APerformer == null)
            {
                throw new CorruptSaveException($"Unable to restore action performer: {savedAction.Performer}");
            }
        }

        if (!savedAction.Target.IsNull)
        {
            action.d20ATarget = GameSystems.Object.GetObject(savedAction.Target);
            if (action.d20ATarget == null)
            {
                throw new CorruptSaveException($"Unable to restore action target: {savedAction.Target}");
            }
        }

        return(action);
    }