/* Steps for adding a new spellbook

Prepared:
Make cls_spgn_*.2da
Make cls_spcr_*.2da
Make blank cls_spell_*.2da
Add cls_spgn_*.2da to classes.2da
Add class entry in prc_classes.2da
Add the spellbook feat (#1999) to cls_feat_*.2da at the appropriate level
Add class to GetSpellbookTypeForClass() below
Add class to GetAbilityScoreForClass() below
Add class to bKnowsAllClassSpells() below if necessary
Add class to GetIsArcaneClass() or GetIsDivineClass() in prc_inc_castlvl as appropriate
Add class to GetCasterLevelModifier() in prc_inc_castlvl if necessary
Add class to SetupLookupStage() in inc_lookups
Add class to GetCasterLvl() in prc_inc_spells
Add Practiced Spellcaster feat to feat.2da and to PracticedSpellcasting() in prc_inc_castlvl
Run the assemble_spellbooks.bat file
Make the prc_* scripts in newspellbook. The filenames can be found under the spell entries for the class in spells.2da.

Spont:
Make cls_spgn_*.2da
Make cls_spkn_*.2da
Make cls_spcr_*.2da
Make blank cls_spell_*.2da
Add cls_spkn_*.2da and cls_spgn_*.2da to classes.2da
Add class entry in prc_classes.2da
Add class to GetSpellbookTypeForClass() below
Add class to GetAbilityScoreForClass() below
Add class to bKnowsAllClassSpells() below if necessary
Add class to GetIsArcaneClass() or GetIsDivineClass() in prc_inc_castlvl as appropriate
Add class to GetCasterLevelModifier() in prc_inc_castlvl if necessary
Add class to SetupLookupStage() in inc_lookups
Add class to GetCasterLvl() in prc_inc_spells
Add Practiced Spellcaster feat to feat.2da and to PracticedSpellcasting() in prc_inc_castlvl
Add class to prc_amagsys_gain if(CheckMissingSpells(oPC, CLASS_TYPE_SORCERER, MinimumSpellLevel, MaximumSpellLevel))
Add class to ExecuteScript("prc_amagsys_gain", oPC) list in EvalPRCFeats in prc_inc_function
Run the assemble_spellbooks.bat file
Make the prc_* scripts in newspellbook

prc_classes.2da entry:
Label       - name for the class
Name        - tlk file strref
SpellCaster - does the class cast spells? 0 = No, 1 = Yes (used for bonus spellslot item properties)
SBType      - S = spontaneous, P = prepared
AL          - does the class use Advanced Learning of any type? 0 = No, 1 = Yes
*/

//////////////////////////////////////////////////
/*             Function prototypes              */
//////////////////////////////////////////////////

int GetSpellbookTypeForClass(int nClass);
int GetAbilityScoreForClass(int nClass, object oPC);

/**
 * Determines the given character's DC-modifying ability modifier for
 * the given class' spells. Handles split-score casters.
 *
 * @param nClass The spellcasting class for whose spells to determine ability mod to DC for
 * @param oPC    The character whose abilities to examine
 * @return       The DC-modifying ability score's ability modifier value
 */
int GetDCAbilityModForClass(int nClass, object oPC);

string GetFileForClass(int nClass);
int GetSpellslotLevel(int nClass, object oPC);
int GetItemBonusSlotCount(object oPC, int nClass, int nSpellLevel);
int GetSlotCount(int nLevel, int nSpellLevel, int nAbilityScore, int nClass, object oItemPosessor = OBJECT_INVALID);
int bKnowsAllClassSpells(int nClass);
int GetSpellKnownMaxCount(int nLevel, int nSpellLevel, int nClass, object oPC);
int GetSpellKnownCurrentCount(object oPC, int nSpellLevel, int nClass);
int GetSpellUnknownCurrentCount(object oPC, int nSpellLevel, int nClass);
void AddSpellUse(object oPC, int nSpellbookID, int nClass, string sFile, string sArrayName, int nSpellbookType, object oSkin, int nFeatID, int nIPFeatID, string sIDX = "");
void RemoveSpellUse(object oPC, int nSpellID, int nClass);
// int GetSpellUses(object oPC, int nSpellID, int nClass);
int GetSpellLevel(int nSpellID, int nClass);
void SetupSpells(object oPC, int nClass);
void CheckAndRemoveFeat(object oHide, itemproperty ipFeat);
void WipeSpellbookHideFeats(object oPC);
void CheckNewSpellbooks(object oPC);
void NewSpellbookSpell(int nClass, int nSpellbookType, int nMetamagic = METAMAGIC_NONE, int bInstantSpell = FALSE);
void CastSpontaneousSpell(int nClass, int bInstantSpell = FALSE);
void CastPreparedSpell(int nClass, int nMetamagic = METAMAGIC_NONE, int bInstantSpell = FALSE);

//////////////////////////////////////////////////
/*                 Constants                    */
//////////////////////////////////////////////////

/*     stored in "prc_inc_sb_const"
    Accessed via "prc_inc_core"    */

//////////////////////////////////////////////////
/*                  Includes                    */
//////////////////////////////////////////////////

// ** THIS ORDER IS IMPORTANT **

//#include "prc_effect_inc"         //access via prc_inc_core
//#include "inc_lookups"            //access via prc_inc_core
#include "prc_inc_core"
#include "inc_sp_gain_mem"          //providing child access to prc_inc_core
                                    //Must load in this order.
//#include "prc_inc_castlvl"        //access via prc_inc_core
//#include "prc_inc_descrptr"       //access via prc_inc_core


//////////////////////////////////////////////////
/*             Function definitions             */
//////////////////////////////////////////////////

int GetSpellbookTypeForClass(int nClass)
{
    switch(nClass)
    {
        case CLASS_TYPE_ARCHIVIST:
        case CLASS_TYPE_BLACKGUARD:
        case CLASS_TYPE_BLIGHTER:
        case CLASS_TYPE_CLERIC:
        case CLASS_TYPE_CULTIST_SHATTERED_PEAK:
        case CLASS_TYPE_DRUID:
        case CLASS_TYPE_HEALER:
        case CLASS_TYPE_KNIGHT_CHALICE:
        case CLASS_TYPE_KNIGHT_MIDDLECIRCLE:
        case CLASS_TYPE_NENTYAR_HUNTER:
        case CLASS_TYPE_OCULAR:
        case CLASS_TYPE_PALADIN:
        case CLASS_TYPE_RANGER:
        case CLASS_TYPE_SHADOWLORD:
        case CLASS_TYPE_SHAMAN:
        case CLASS_TYPE_SLAYER_OF_DOMIEL:
        case CLASS_TYPE_SOHEI:
        case CLASS_TYPE_SOLDIER_OF_LIGHT:
        case CLASS_TYPE_UR_PRIEST:
        case CLASS_TYPE_VASSAL:
        case CLASS_TYPE_VIGILANT:
        case CLASS_TYPE_WIZARD:
            return SPELLBOOK_TYPE_PREPARED;
        case CLASS_TYPE_ASSASSIN:
        case CLASS_TYPE_BARD:
        case CLASS_TYPE_BEGUILER:
        case CLASS_TYPE_CELEBRANT_SHARESS:
        case CLASS_TYPE_DREAD_NECROMANCER:
        case CLASS_TYPE_DUSKBLADE:
        case CLASS_TYPE_FAVOURED_SOUL:
        case CLASS_TYPE_HARPER:
        case CLASS_TYPE_HEXBLADE:
        case CLASS_TYPE_JUSTICEWW:
        case CLASS_TYPE_KNIGHT_WEAVE:
        case CLASS_TYPE_SORCERER:
        case CLASS_TYPE_SUBLIME_CHORD:
        case CLASS_TYPE_SUEL_ARCHANAMACH:
        case CLASS_TYPE_WARMAGE:
            return SPELLBOOK_TYPE_SPONTANEOUS;
		// shapechanger HD count as sorcerer for aranea.	
		case CLASS_TYPE_SHAPECHANGER: 
			return SPELLBOOK_TYPE_SPONTANEOUS;
		// Multiple races	
		case CLASS_TYPE_MONSTROUS: 
			return SPELLBOOK_TYPE_SPONTANEOUS;			
		// Gloura as Bard	
		case CLASS_TYPE_FEY: 
			return SPELLBOOK_TYPE_SPONTANEOUS;			
		// Drider as Sorc	
		case CLASS_TYPE_ABERRATION: 
			return SPELLBOOK_TYPE_SPONTANEOUS;			
        //outsider HD count as sorc for raks
        case CLASS_TYPE_OUTSIDER: {
            /// @todo Will eventually need to add a check here to differentiate between races. Not all are sorcerers, just most
            return SPELLBOOK_TYPE_SPONTANEOUS;
        }
    }
    return SPELLBOOK_TYPE_INVALID;
}

int GetAbilityScoreForClass(int nClass, object oPC)
{
    switch(nClass)
    {
        case CLASS_TYPE_BLACKGUARD:
        case CLASS_TYPE_BLIGHTER:
        case CLASS_TYPE_CLERIC:
        case CLASS_TYPE_DRUID:
        case CLASS_TYPE_FIST_OF_ZUOKEN:
        case CLASS_TYPE_HEALER:
        case CLASS_TYPE_JUSTICEWW:
        case CLASS_TYPE_KNIGHT_CHALICE:
        case CLASS_TYPE_KNIGHT_MIDDLECIRCLE:
        case CLASS_TYPE_NENTYAR_HUNTER:
        case CLASS_TYPE_OCULAR:
        case CLASS_TYPE_PALADIN:
        case CLASS_TYPE_PSYWAR:
        case CLASS_TYPE_RANGER:
        case CLASS_TYPE_SHAMAN:
        case CLASS_TYPE_SLAYER_OF_DOMIEL:
        case CLASS_TYPE_SOHEI:
        case CLASS_TYPE_SOLDIER_OF_LIGHT:
        case CLASS_TYPE_UR_PRIEST:
        case CLASS_TYPE_VASSAL:
        case CLASS_TYPE_VIGILANT:
        case CLASS_TYPE_WARMIND:
            return GetAbilityScore(oPC, ABILITY_WISDOM);
        case CLASS_TYPE_ARCHIVIST:
        case CLASS_TYPE_ASSASSIN:
        case CLASS_TYPE_BEGUILER:
        case CLASS_TYPE_CULTIST_SHATTERED_PEAK:
        case CLASS_TYPE_DUSKBLADE:
        case CLASS_TYPE_PSION:
        case CLASS_TYPE_PSYCHIC_ROGUE:
        case CLASS_TYPE_SHADOWCASTER:
        case CLASS_TYPE_SHADOWLORD:
        case CLASS_TYPE_WIZARD:
            return GetAbilityScore(oPC, ABILITY_INTELLIGENCE);
        case CLASS_TYPE_BARD:
        case CLASS_TYPE_CELEBRANT_SHARESS:
        case CLASS_TYPE_DREAD_NECROMANCER:
        case CLASS_TYPE_FAVOURED_SOUL:
        case CLASS_TYPE_HARPER:
        case CLASS_TYPE_HEXBLADE:
        case CLASS_TYPE_KNIGHT_WEAVE:
        case CLASS_TYPE_SORCERER:
        case CLASS_TYPE_SUBLIME_CHORD:
        case CLASS_TYPE_SUEL_ARCHANAMACH:
        case CLASS_TYPE_WARMAGE:
        case CLASS_TYPE_WILDER:
            return GetAbilityScore(oPC, ABILITY_CHARISMA);
        //shapeshifter HD count as sorc for aranea
        case CLASS_TYPE_SHAPECHANGER: 
        	return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Multiple races	
		case CLASS_TYPE_MONSTROUS: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Gloura as Bard	
		case CLASS_TYPE_FEY: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Drider as Sorc	
		case CLASS_TYPE_ABERRATION: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);
        //outsider HD count as sorc for raks
        case CLASS_TYPE_OUTSIDER: {
            /// @todo Will eventually need to add a check here to differentiate between races. Not all are sorcerers, just most
            return GetAbilityScore(oPC, ABILITY_CHARISMA);
        }
    }
    return GetAbilityScore(oPC, ABILITY_CHARISMA);    //default for SLAs?
}

int GetDCAbilityModForClass(int nClass, object oPC)
{
    switch(nClass)
    {
        case CLASS_TYPE_BLACKGUARD:
        case CLASS_TYPE_BLIGHTER:
        case CLASS_TYPE_CLERIC:
        case CLASS_TYPE_DRUID:
        case CLASS_TYPE_FAVOURED_SOUL:
        case CLASS_TYPE_FIST_OF_ZUOKEN:
        case CLASS_TYPE_JUSTICEWW:
        case CLASS_TYPE_KNIGHT_CHALICE:
        case CLASS_TYPE_KNIGHT_MIDDLECIRCLE:
        case CLASS_TYPE_OCULAR:
        case CLASS_TYPE_NENTYAR_HUNTER:
        case CLASS_TYPE_PALADIN:
        case CLASS_TYPE_PSYWAR:
        case CLASS_TYPE_RANGER:
        case CLASS_TYPE_SHAMAN:
        case CLASS_TYPE_SLAYER_OF_DOMIEL:
        case CLASS_TYPE_SOHEI:
        case CLASS_TYPE_SOLDIER_OF_LIGHT:
        case CLASS_TYPE_UR_PRIEST:
        case CLASS_TYPE_VASSAL:
        case CLASS_TYPE_VIGILANT:
        case CLASS_TYPE_WARMIND:
            return GetAbilityModifier(ABILITY_WISDOM, oPC);
        case CLASS_TYPE_ARCHIVIST:
        case CLASS_TYPE_ASSASSIN:
        case CLASS_TYPE_BEGUILER:
        case CLASS_TYPE_CULTIST_SHATTERED_PEAK:
        case CLASS_TYPE_DUSKBLADE:        
        case CLASS_TYPE_PSION:
        case CLASS_TYPE_PSYCHIC_ROGUE:
        case CLASS_TYPE_SHADOWLORD:
        case CLASS_TYPE_WIZARD:
            return GetAbilityModifier(ABILITY_INTELLIGENCE, oPC);
        case CLASS_TYPE_BARD:
        case CLASS_TYPE_CELEBRANT_SHARESS:
        case CLASS_TYPE_DREAD_NECROMANCER:
        case CLASS_TYPE_HARPER:
        case CLASS_TYPE_HEALER:
        case CLASS_TYPE_HEXBLADE:
        case CLASS_TYPE_SHADOWCASTER:
        case CLASS_TYPE_SORCERER:
        case CLASS_TYPE_SUBLIME_CHORD:
        case CLASS_TYPE_SUEL_ARCHANAMACH:
        case CLASS_TYPE_WARMAGE:
        case CLASS_TYPE_WILDER:
            return GetAbilityModifier(ABILITY_CHARISMA, oPC);
        //shapechanger HD count as sorc for aranea
        case CLASS_TYPE_SHAPECHANGER: 
        	return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Multiple races	
		case CLASS_TYPE_MONSTROUS: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Gloura as Bard	
		case CLASS_TYPE_FEY: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);
		// Drider as Sorc	
		case CLASS_TYPE_ABERRATION: 
			return GetAbilityScore(oPC, ABILITY_CHARISMA);		
        //outsider HD count as sorc for raks
        case CLASS_TYPE_OUTSIDER: {
            /// @todo Will eventually need to add a check here to differentiate between races. Not all are sorcerers, just most
            return GetAbilityModifier(ABILITY_CHARISMA, oPC);
        }
    }
    return GetAbilityModifier(ABILITY_CHARISMA, oPC);    //default for SLAs?
}

string GetFileForClass(int nClass)
{
    string sFile = Get2DACache("classes", "FeatsTable", nClass);
    sFile = "cls_spell" + GetStringRight(sFile, GetStringLength(sFile) - 8); // Hardcoded the cls_ part. It's not as if any class uses some other prefix - Ornedan, 20061231
    //if(DEBUG) DoDebug("GetFileForClass(" + IntToString(nClass) + ") = " + sFile);
    return sFile;
}

int GetSpellslotLevel(int nClass, object oPC)
{
    int nLevel = GetLevelByClass(nClass, oPC);
    
    //Raks cast as sorcs
    if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_RAKSHASA)
        nLevel = GetLevelByClass(CLASS_TYPE_OUTSIDER, oPC);
    else if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_ARKAMOI) //Arkamoi cast as sorcs
        nLevel = GetLevelByClass(CLASS_TYPE_MONSTROUS, oPC);
    else if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_DRIDER) //Driders cast as sorcs
        nLevel = GetLevelByClass(CLASS_TYPE_ABERRATION, oPC);   
    else if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_REDSPAWN_ARCANISS) //Redspawn Arcaniss cast as 3/4 sorcs
        nLevel = GetLevelByClass(CLASS_TYPE_MONSTROUS, oPC)*3/4;  
    else if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_MARRUTACT) //Marrutact cast as 6/7 sorcs
        nLevel = GetLevelByClass(CLASS_TYPE_MONSTROUS, oPC)*6/7;          
    else if(nClass == CLASS_TYPE_BARD && !GetLevelByClass(CLASS_TYPE_BARD, oPC) && GetRacialType(oPC) == RACIAL_TYPE_GLOURA) //Gloura cast as bards
        nLevel = GetLevelByClass(CLASS_TYPE_FEY, oPC);         
    else if(nClass == CLASS_TYPE_SORCERER && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC) && GetRacialType(oPC) == RACIAL_TYPE_ARANEA) //Aranea cast as sorcs
        nLevel = GetLevelByClass(CLASS_TYPE_SHAPECHANGER, oPC);         
    
    int nArcSpellslotLevel;
    int nDivSpellslotLevel;
    int i;
    for(i = 1; i <= 3; i++)
    {
        int nTempClass = GetClassByPosition(i, oPC);
        //spellcasting prc
        int nArcSpellMod = StringToInt(Get2DACache("classes", "ArcSpellLvlMod", nTempClass));
        int nDivSpellMod = StringToInt(Get2DACache("classes", "DivSpellLvlMod", nTempClass));
        /*//special case for combat medic class
        if(nTempClass == CLASS_TYPE_COMBAT_MEDIC && (nClass == CLASS_TYPE_BARD || nClass == CLASS_TYPE_WITCH))
            nArcSpellMod = 1;*/

        if(nArcSpellMod == 1)
            nArcSpellslotLevel += GetLevelByClass(nTempClass, oPC);
        else if(nArcSpellMod > 1)
            nArcSpellslotLevel += (GetLevelByClass(nTempClass, oPC) + 1) / nArcSpellMod;
        if(nDivSpellMod == 1)
            nDivSpellslotLevel += GetLevelByClass(nTempClass, oPC);
        else if(nDivSpellMod > 1)
            nDivSpellslotLevel += (GetLevelByClass(nTempClass, oPC) + 1) / nDivSpellMod;
    }
    
    if(GetPrimaryArcaneClass(oPC) == nClass)
        nLevel += nArcSpellslotLevel;
    if(GetPrimaryDivineClass(oPC) == nClass)
        nLevel += nDivSpellslotLevel;
        
    // For this special instance, we know that this is the only prestige class
    if (nClass == CLASS_TYPE_SORCERER && GetLevelByClass(CLASS_TYPE_ULTIMATE_MAGUS, oPC))
    	nLevel = GetLevelByClass(CLASS_TYPE_ULTIMATE_MAGUS, oPC) + GetLevelByClass(CLASS_TYPE_SORCERER, oPC);
    	
    if(DEBUG) DoDebug("GetSpellslotLevel(" + IntToString(nClass) + ", " + GetName(oPC) + ") = " + IntToString(nLevel));
    return nLevel;
}

int GetItemBonusSlotCount(object oPC, int nClass, int nSpellLevel)
{
    // Value maintained by CheckPRCLimitations()
    return GetLocalInt(oPC, "PRC_IPRPBonSpellSlots_" + IntToString(nClass) + "_" + IntToString(nSpellLevel));
}

int GetSlotCount(int nLevel, int nSpellLevel, int nAbilityScore, int nClass, object oItemPosessor = OBJECT_INVALID)
{
    // Ability score limit rule: Must have casting ability score of at least 10 + spel level to be able to cast spells of that level at all
    if(nAbilityScore < nSpellLevel + 10)
        return 0;
    int nSlots;
    string sFile;
    /*// Bioware casters use their classes.2da-specified tables
    if(    nClass == CLASS_TYPE_WIZARD
        || nClass == CLASS_TYPE_SORCERER
        || nClass == CLASS_TYPE_BARD
        || nClass == CLASS_TYPE_CLERIC
        || nClass == CLASS_TYPE_DRUID
        || nClass == CLASS_TYPE_PALADIN
        || nClass == CLASS_TYPE_RANGER)
    {*/
        sFile = Get2DACache("classes", "SpellGainTable", nClass);
    /*}
    // New spellbook casters use the cls_spbk_* tables
    else
    {
        sFile = Get2DACache("classes", "FeatsTable", nClass);
        sFile = "cls_spbk" + GetStringRight(sFile, GetStringLength(sFile) - 8); // Hardcoded the cls_ part. It's not as if any class uses some other prefix - Ornedan, 20061231
    }*/

    string sSlots = Get2DACache(sFile, "SpellLevel" + IntToString(nSpellLevel), nLevel - 1);
    if(sSlots == "")
    {
        nSlots = -1;
        //if(DEBUG) DoDebug("GetSlotCount: Problem getting slot numbers for " + IntToString(nSpellLevel) + " " + IntToString(nLevel) + " " + sFile);
    }
    else
        nSlots = StringToInt(sSlots);
    if(nSlots == -1)
        return 0;

    // Add spell slots from items
    if(GetIsObjectValid(oItemPosessor))
        nSlots += GetItemBonusSlotCount(oItemPosessor, nClass, nSpellLevel);

    // Add spell slots from high ability score. Level 0 spells are exempt
    if(nSpellLevel == 0)
        return nSlots;
    else 
    {
        int nAbilityMod = nClass == CLASS_TYPE_ARCHIVIST ? GetAbilityModifier(ABILITY_WISDOM, oItemPosessor) : (nAbilityScore - 10) / 2;
        if(nAbilityMod >= nSpellLevel) // Need an ability modifier at least equal to the spell level to gain bonus slots
            nSlots += ((nAbilityMod - nSpellLevel) / 4) + 1;
        return nSlots;
    }
}

//if the class doesn't learn all available spells on level-up add it here
int bKnowsAllClassSpells(int nClass)
{
    switch(nClass)
    {
        //case CLASS_TYPE_WIZARD:
        case CLASS_TYPE_ARCHIVIST:
        case CLASS_TYPE_ASSASSIN:
        case CLASS_TYPE_BARD:
        case CLASS_TYPE_CELEBRANT_SHARESS:
        case CLASS_TYPE_CULTIST_SHATTERED_PEAK:
        case CLASS_TYPE_DUSKBLADE:
        case CLASS_TYPE_FAVOURED_SOUL:
        case CLASS_TYPE_HEXBLADE:
        case CLASS_TYPE_JUSTICEWW:
        case CLASS_TYPE_KNIGHT_WEAVE:
        case CLASS_TYPE_SORCERER:
        case CLASS_TYPE_SUBLIME_CHORD:
        case CLASS_TYPE_SUEL_ARCHANAMACH:
            return FALSE;

        // Everyone else
        default:
            return TRUE;
    }
    return TRUE;
}

int GetSpellKnownMaxCount(int nLevel, int nSpellLevel, int nClass, object oPC)
{
    // If the character doesn't have any spell slots available on for this level, it can't know any spells of that level either
    /// @todo Check rules. There might be cases where this doesn't hold
    if(!GetSlotCount(nLevel, nSpellLevel, GetAbilityScoreForClass(nClass, oPC), nClass))
        return 0;
    int nKnown;
    string sFile;
    // Bioware casters use their classes.2da-specified tables
    /*if(    nClass == CLASS_TYPE_WIZARD
        || nClass == CLASS_TYPE_SORCERER
        || nClass == CLASS_TYPE_BARD
        || nClass == CLASS_TYPE_CLERIC
        || nClass == CLASS_TYPE_DRUID
        || nClass == CLASS_TYPE_PALADIN
        || nClass == CLASS_TYPE_RANGER)
    {*/
        sFile = Get2DACache("classes", "SpellKnownTable", nClass);
    /*}
    else
    {
        sFile = Get2DACache("classes", "FeatsTable", nClass);
        sFile = "cls_spkn" + GetStringRight(sFile, GetStringLength(sFile) - 8); // Hardcoded the cls_ part. It's not as if any class uses some other prefix - Ornedan, 20061231
    }*/

    string sKnown = Get2DACache(sFile, "SpellLevel" + IntToString(nSpellLevel), nLevel - 1);
    if(DEBUG) DoDebug("GetSpellKnownMaxCount(" + IntToString(nLevel) + ", " + IntToString(nSpellLevel) + ", " + IntToString(nClass) + ", " + GetName(oPC) + ") = " + sKnown);
    if(sKnown == "")
    {
        nKnown = -1;
        //if(DEBUG) DoDebug("GetSpellKnownMaxCount: Problem getting known numbers for " + IntToString(nSpellLevel) + " " + IntToString(nLevel) + " " + sFile);
    }
    else
        nKnown = StringToInt(sKnown);
    if(nKnown == -1)
        return 0;

    // Bard and Sorcerer only have new spellbook spells known if they have taken prestige classes that increase spellcasting
    if(nClass == CLASS_TYPE_SORCERER || nClass == CLASS_TYPE_BARD)
    {
        if((GetLevelByClass(nClass) == nLevel) //no PrC
          && !(GetHasFeat(FEAT_DRACONIC_GRACE, oPC) || GetHasFeat(FEAT_DRACONIC_BREATH, oPC))) //no Draconic feats that apply
            return 0;
    }
    return nKnown;
}

int GetSpellKnownCurrentCount(object oPC, int nSpellLevel, int nClass)
{
    // Check short-term cache
    string sClassNum = IntToString(nClass);
    if(GetLocalInt(oPC, "GetSKCCCache_" + IntToString(nSpellLevel) + "_" + sClassNum))
        return GetLocalInt(oPC, "GetSKCCCache_" + IntToString(nSpellLevel) + "_" + sClassNum) - 1;

    // Loop over all spells known and count the number of spells of each level known
    int i;
    int nKnown;
    int nKnown0, nKnown1, nKnown2, nKnown3, nKnown4;
    int nKnown5, nKnown6, nKnown7, nKnown8, nKnown9;
    string sFile = GetFileForClass(nClass);
    for(i = 0; i < persistant_array_get_size(oPC, "Spellbook" + sClassNum); i++)
    {
        int nNewSpellbookID = persistant_array_get_int(oPC, "Spellbook" + sClassNum, i);
        int nLevel = StringToInt(Get2DACache(sFile, "Level", nNewSpellbookID));
        switch(nLevel)
        {
            case 0: nKnown0++; break; case 1: nKnown1++; break;
            case 2: nKnown2++; break; case 3: nKnown3++; break;
            case 4: nKnown4++; break; case 5: nKnown5++; break;
            case 6: nKnown6++; break; case 7: nKnown7++; break;
            case 8: nKnown8++; break; case 9: nKnown9++; break;
        }
    }

    // Pick the level requested for returning
    switch(nSpellLevel)
    {
        case 0: nKnown = nKnown0; break; case 1: nKnown = nKnown1; break;
        case 2: nKnown = nKnown2; break; case 3: nKnown = nKnown3; break;
        case 4: nKnown = nKnown4; break; case 5: nKnown = nKnown5; break;
        case 6: nKnown = nKnown6; break; case 7: nKnown = nKnown7; break;
        case 8: nKnown = nKnown8; break; case 9: nKnown = nKnown9; break;
    }
    if(DEBUG) DoDebug("GetSpellKnownCurrentCount(" + GetName(oPC) + ", " + IntToString(nSpellLevel) + ", " + sClassNum + ") = " + IntToString(nKnown));
    if(DEBUG) DoDebug("GetSpellKnownCurrentCount(i " + IntToString(i) + ", nKnown0 " + IntToString(nKnown0) + ", nKnown1 " + IntToString(nKnown1) + ", nKnown2 " + IntToString(nKnown2) + ", nKnown3 " + IntToString(nKnown3) + ", nKnown4 " + IntToString(nKnown4) + ", nKnown5 " + IntToString(nKnown5) + ", nKnown6 " + IntToString(nKnown6) + ", nKnown7 " + IntToString(nKnown7) + ", nKnown8 " + IntToString(nKnown8) + ", nKnown9 " + IntToString(nKnown9));
    if(DEBUG) DoDebug("GetSpellKnownCurrentCount(persistant_array_get_size "+IntToString(persistant_array_get_size(oPC, "Spellbook" + sClassNum)));

    // Cache the values for 1 second
    SetLocalInt(oPC, "GetSKCCCache_0_" + sClassNum, nKnown0 + 1);
    SetLocalInt(oPC, "GetSKCCCache_1_" + sClassNum, nKnown1 + 1);
    SetLocalInt(oPC, "GetSKCCCache_2_" + sClassNum, nKnown2 + 1);
    SetLocalInt(oPC, "GetSKCCCache_3_" + sClassNum, nKnown3 + 1);
    SetLocalInt(oPC, "GetSKCCCache_4_" + sClassNum, nKnown4 + 1);
    SetLocalInt(oPC, "GetSKCCCache_5_" + sClassNum, nKnown5 + 1);
    SetLocalInt(oPC, "GetSKCCCache_6_" + sClassNum, nKnown6 + 1);
    SetLocalInt(oPC, "GetSKCCCache_7_" + sClassNum, nKnown7 + 1);
    SetLocalInt(oPC, "GetSKCCCache_8_" + sClassNum, nKnown8 + 1);
    SetLocalInt(oPC, "GetSKCCCache_9_" + sClassNum, nKnown9 + 1);
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_0_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_1_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_2_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_3_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_4_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_5_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_6_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_7_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_8_" + sClassNum));
    DelayCommand(1.0, DeleteLocalInt(oPC, "GetSKCCCache_9_" + sClassNum));

    return nKnown;
}

int GetSpellUnknownCurrentCount(object oPC, int nSpellLevel, int nClass)
{
    // Get the lookup token created by MakeSpellbookLevelLoop()
    string sTag = "SpellLvl_" + IntToString(nClass) + "_Level_" + IntToString(nSpellLevel);
    object oCache = GetObjectByTag(sTag);
    if(!GetIsObjectValid(oCache))
    {
        if(DEBUG) DoDebug("GetSpellUnknownCurrentCount: " + sTag + " is not valid");
        return 0;
    }
    // Read the total number of spells on the given level and determine how many are already known
    int nTotal = array_get_size(oCache, "Lkup");
    int nKnown = GetSpellKnownCurrentCount(oPC, nSpellLevel, nClass);
    int nUnknown = nTotal - nKnown;

    if(DEBUG) DoDebug("GetSpellUnknownCurrentCount(" + GetName(oPC) + ", " + IntToString(nSpellLevel) + ", " + IntToString(nClass) + ") = " + IntToString(nUnknown));
    return nUnknown;
}

void AddSpellUse(object oPC, int nSpellbookID, int nClass, string sFile, string sArrayName, int nSpellbookType, object oSkin, int nFeatID, int nIPFeatID, string sIDX = "")
{
    /*
    string sFile = GetFileForClass(nClass);
    string sArrayName = "NewSpellbookMem_"+IntToString(nClass);
    int nSpellbookType = GetSpellbookTypeForClass(nClass);
    object oSkin = GetPCSkin(oPC);
    int nFeatID = StringToInt(Get2DACache(sFile, "FeatID", nSpellbookID));
    //add the feat only if they dont already have it
    int nIPFeatID = StringToInt(Get2DACache(sFile, "IPFeatID", nSpellbookID));
    */
    object oToken = GetHideToken(oPC);

    // Add the spell use feats and set a marker local that tells for CheckAndRemoveFeat() to skip removing this feat
    string sIPFeatID = IntToString(nIPFeatID);
    SetLocalInt(oSkin, "NewSpellbookTemp_" + sIPFeatID, TRUE);
    AddSkinFeat(nFeatID, nIPFeatID, oSkin, oPC);

    // Increase the current number of uses
    if(nSpellbookType == SPELLBOOK_TYPE_PREPARED)
    {
        //sanity test
        if(!persistant_array_exists(oPC, sArrayName))
        {
            if(DEBUG) DoDebug("ERROR: AddSpellUse: " + sArrayName + " array does not exist, creating");
            persistant_array_create(oPC, sArrayName);
        }

        int nUses = persistant_array_get_int(oPC, sArrayName, nSpellbookID);
        nUses++;
        persistant_array_set_int(oPC, sArrayName, nSpellbookID, nUses);
        if(DEBUG) DoDebug("AddSpellUse: " + sArrayName + "[" + IntToString(nSpellbookID) + "] = " + IntToString(array_get_int(oPC, sArrayName, nSpellbookID)));

        //Create index array - to avoid duplicates mark only 1st use of nSpellbookID
        if(nUses == 1)
        {
            if(!persistant_array_exists(oPC, sIDX))
                persistant_array_create(oPC, sIDX);

            persistant_array_set_int(oPC, sIDX, array_get_size(oPC, sIDX), nSpellbookID);
        }
    }
    else if(nSpellbookType == SPELLBOOK_TYPE_SPONTANEOUS)
    {
        //sanity test
        if(!persistant_array_exists(oPC, sArrayName))
        {
            if(DEBUG) DoDebug("ERROR: AddSpellUse: " + sArrayName + " array does not exist, creating");
            persistant_array_create(oPC, sArrayName);
        }
        /*int nSpellLevel = StringToInt(Get2DACache(sFile, "Level", nSpellbookID));
        int nCount = persistant_array_get_int(oPC, sArrayName, nSpellLevel);
        if(nCount < 1)
        {
            int nLevel = GetSpellslotLevel(nClass, oPC);
            int nAbility = GetAbilityScoreForClass(nClass, oPC);
            nCount = GetSlotCount(nLevel, nSpellLevel, nAbility, nClass, oPC);
            array_set_int(oPC, sArrayName, nSpellLevel, nCount);
        }*/
        if(DEBUG) DoDebug("AddSpellUse() called on spontaneous spellbook. nIPFeatID = " + sIPFeatID);
    }
}

void RemoveSpellUse(object oPC, int nSpellID, int nClass)
{
    string sFile = GetFileForClass(nClass);
    int nSpellbookID = SpellToSpellbookID(nSpellID);
    if(nSpellbookID == -1)
    {
        if(DEBUG) DoDebug("ERROR: RemoveSpellUse: Unable to resolve spell to spellbookID: " + IntToString(nSpellID) + " in file " + sFile);
        return;
    }
    if(!persistant_array_exists(oPC, "NewSpellbookMem_"+IntToString(nClass)))
    {
        if(DEBUG) DoDebug("RemoveSpellUse: NewSpellbookMem_" + IntToString(nClass) + " does not exist, creating.");
        persistant_array_create(oPC, "NewSpellbookMem_"+IntToString(nClass));
    }

    // Reduce the remaining uses of the given spell by 1 (except never reduce uses below 0).
    // Spontaneous spellbooks reduce the number of spells of the spell's level remaining
    int nSpellbookType = GetSpellbookTypeForClass(nClass);
    if(nSpellbookType == SPELLBOOK_TYPE_PREPARED)
    {
        int nCount = persistant_array_get_int(oPC, "NewSpellbookMem_" + IntToString(nClass), nSpellbookID);
        if(nCount > 0)
            persistant_array_set_int(oPC, "NewSpellbookMem_" + IntToString(nClass), nSpellbookID, nCount - 1);
    }
    else if(nSpellbookType == SPELLBOOK_TYPE_SPONTANEOUS)
    {
        int nSpellLevel = StringToInt(Get2DACache(sFile, "Level", nSpellbookID));
        int nCount = persistant_array_get_int(oPC, "NewSpellbookMem_" + IntToString(nClass), nSpellLevel);
        if(nCount > 0)
            persistant_array_set_int(oPC, "NewSpellbookMem_" + IntToString(nClass), nSpellLevel, nCount - 1);
    }
}

int GetSpellLevel(int nSpellID, int nClass)
{
    string sFile = GetFileForClass(nClass);
    int nSpellbookID = SpellToSpellbookID(nSpellID);
    if(nSpellbookID == -1)
    {
        if(DEBUG) DoDebug("ERROR: GetSpellLevel: Unable to resolve spell to spellbookID: "+IntToString(nSpellID)+" "+sFile);
        return -1;
    }

    // get spell level
    int nSpellLevel = -1;
    string sSpellLevel = Get2DACache(sFile, "Level", nSpellbookID);

    if (sSpellLevel != "")
        nSpellLevel = StringToInt(sSpellLevel);

    return nSpellLevel;
}

//called inside for loop in SetupSpells(), delayed to prevent TMI
void SpontaneousSpellSetupLoop(object oPC, int nClass, string sFile, object oSkin, int i)
{
    int nSpellbookID = persistant_array_get_int(oPC, "Spellbook" + IntToString(nClass), i);
    string sIPFeatID = Get2DACache(sFile, "IPFeatID", nSpellbookID);
    int nIPFeatID = StringToInt(sIPFeatID);
    int nFeatID = StringToInt(Get2DACache(sFile, "FeatID", nSpellbookID));
    //int nRealSpellID = StringToInt(Get2DACache(sFile, "RealSpellID", nSpellbookID));
    SetLocalInt(oSkin, "NewSpellbookTemp_" + sIPFeatID, TRUE);

    AddSkinFeat(nFeatID, nIPFeatID, oSkin, oPC);
}

void SetupSpells(object oPC, int nClass)
{
    string sFile = GetFileForClass(nClass);
    string sClass = IntToString(nClass);
    string sArrayName = "NewSpellbookMem_" + sClass;
    object oSkin = GetPCSkin(oPC);
    int nLevel = GetSpellslotLevel(nClass, oPC);
    int nAbility = GetAbilityScoreForClass(nClass, oPC);
    int nSpellbookType = GetSpellbookTypeForClass(nClass);
    
        if(DEBUG) DoDebug("SetupSpells\n"
                        + "nClass = " + IntToString(nClass) + "\n"
                        + "nSpellslotLevel = " + IntToString(nLevel) + "\n"
                        + "nAbility = " + IntToString(nAbility) + "\n"
                        + "nSpellbookType = " + IntToString(nSpellbookType) + "\n"
                        + "sFile = " + sFile + "\n"
                          );    

    // For spontaneous spellbooks, set up an array that tells how many spells of each level they can cast
    // And add casting feats for each spell known to the caster's hide

    if(nSpellbookType == SPELLBOOK_TYPE_SPONTANEOUS)
    {
        // Spell slots
        int nSpellLevel, nSlots;
        for(nSpellLevel = 0; nSpellLevel <= 9; nSpellLevel++)
        {
            nSlots = GetSlotCount(nLevel, nSpellLevel, nAbility, nClass, oPC);
            persistant_array_set_int(oPC, sArrayName, nSpellLevel, nSlots);
        }

        int i;
        for(i = 0; i < persistant_array_get_size(oPC, "Spellbook" + sClass); i++)
        {   //adding feats
            SpontaneousSpellSetupLoop(oPC, nClass, sFile, oSkin, i);
        }
    }// end if - Spontaneous spellbook

    // For prepared spellbooks, add spell uses and use feats according to spells memorised list
    else if(nSpellbookType == SPELLBOOK_TYPE_PREPARED && !GetIsBioSpellCastClass(nClass))
    {
        int nSpellLevel, nSlot, nSlots, nSpellbookID;
        string sArrayName2, sIDX;
        for(nSpellLevel = 0; nSpellLevel <= 9; nSpellLevel++)
        {
            sArrayName2 = "Spellbook" + IntToString(nSpellLevel) + "_" + sClass; // Minor optimisation: cache the array name string for multiple uses
            sIDX = "SpellbookIDX" + IntToString(nSpellLevel) + "_" + sClass;
            nSlots = GetSlotCount(nLevel, nSpellLevel, nAbility, nClass, oPC);
            nSlot;
            for(nSlot = 0; nSlot < nSlots; nSlot++)
            {
                //done when spells are added to it
                nSpellbookID = persistant_array_get_int(oPC, sArrayName2, nSlot);
                if(nSpellbookID != 0)
                {
                    AddSpellUse(oPC, nSpellbookID, nClass, sFile, sArrayName, nSpellbookType, oSkin,
                        StringToInt(Get2DACache(sFile, "FeatID", nSpellbookID)),
                        StringToInt(Get2DACache(sFile, "IPFeatID", nSpellbookID)),
                        sIDX);
                }
            }
        }
    }
}

void CheckAndRemoveFeat(object oHide, itemproperty ipFeat)
{
    int nSubType = GetItemPropertySubType(ipFeat);
    if(!GetLocalInt(oHide, "NewSpellbookTemp_" + IntToString(nSubType)))
    {
        RemoveItemProperty(oHide, ipFeat);
        DeleteLocalInt(oHide, "NewSpellbookTemp_" + IntToString(nSubType));
        if(DEBUG) DoDebug("CheckAndRemoveFeat: DeleteLocalInt(oHide, NewSpellbookTemp_" + IntToString(nSubType) + ");");
        if(DEBUG) DoDebug("CheckAndRemoveFeat: Removing item property");
    }
    else
    {
        DeleteLocalInt(oHide, "NewSpellbookTemp_" + IntToString(nSubType));
        if(DEBUG) DoDebug("CheckAndRemoveFeat: DeleteLocalInt(oHide, NewSpellbookTemp_" + IntToString(nSubType) + ");");
    }
}

void WipeSpellbookHideFeats(object oPC)
{
    object oHide = GetPCSkin(oPC);
    itemproperty ipTest = GetFirstItemProperty(oHide);
    while(GetIsItemPropertyValid(ipTest))
    {
        int nSubType = GetItemPropertySubType(ipTest);
        if(GetItemPropertyType(ipTest) == ITEM_PROPERTY_BONUS_FEAT &&
              ((nSubType > SPELLBOOK_IPRP_FEATS_START && nSubType < SPELLBOOK_IPRP_FEATS_END) ||
               (nSubType > SPELLBOOK_IPRP_FEATS_START2 && nSubType < SPELLBOOK_IPRP_FEATS_END2))
          )
        {
            DelayCommand(1.0f, CheckAndRemoveFeat(oHide, ipTest));
        }
        ipTest = GetNextItemProperty(oHide);
    }
}

void CheckNewSpellbooks(object oPC)
{
    WipeSpellbookHideFeats(oPC);
    int i;
    for(i = 1; i <= 3; i++)
    {
        int nClass = GetClassByPosition(i, oPC);
        int nLevel = GetLevelByClass(nClass, oPC);

        if(DEBUG) DoDebug("CheckNewSpellbooks\n"
                        + "nClass = " + IntToString(nClass) + "\n"
                        + "nLevel = " + IntToString(nLevel) + "\n"
                          );
        //if bard/sorc newspellbook is disabled after selecting
        //remove those from radial
        if(   (GetPRCSwitch(PRC_BARD_DISALLOW_NEWSPELLBOOK) && nClass == CLASS_TYPE_BARD)
            ||(GetPRCSwitch(PRC_SORC_DISALLOW_NEWSPELLBOOK) && nClass == CLASS_TYPE_SORCERER))
        {
            //do nothing
        }
        else if(nLevel)
        {
			//Aranea cast as sorcs
            if(nClass == CLASS_TYPE_SHAPECHANGER
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_ARANEA)
                nClass = CLASS_TYPE_SORCERER;
            //raks cast as sorcs
            if(nClass == CLASS_TYPE_OUTSIDER
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_RAKSHASA)
                nClass = CLASS_TYPE_SORCERER;
                
            //Arkamoi cast as sorcs
            if(nClass == CLASS_TYPE_MONSTROUS
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_ARKAMOI)                
                nClass = CLASS_TYPE_SORCERER;
                
            //Redspawn cast as sorcs
            if(nClass == CLASS_TYPE_MONSTROUS
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_REDSPAWN_ARCANISS)                
                nClass = CLASS_TYPE_SORCERER;    
                
            //Marrutact cast as sorcs
            if(nClass == CLASS_TYPE_MONSTROUS
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_MARRUTACT)                
                nClass = CLASS_TYPE_SORCERER;                 
                
            //Driders cast as sorcs
            if(nClass == CLASS_TYPE_ABERRATION
                && !GetLevelByClass(CLASS_TYPE_SORCERER, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_DRIDER)                
                nClass = CLASS_TYPE_SORCERER;    
                
            //Gloura cast as bards
            if(nClass == CLASS_TYPE_FEY
                && !GetLevelByClass(CLASS_TYPE_BARD, oPC)
                && GetRacialType(oPC) == RACIAL_TYPE_GLOURA)                
                nClass = CLASS_TYPE_BARD;                  
            //remove persistant locals used to track when all spells cast
            string sArrayName = "NewSpellbookMem_"+IntToString(nClass);
            if(persistant_array_exists(oPC, sArrayName))
            {
                if(GetSpellbookTypeForClass(nClass) == SPELLBOOK_TYPE_PREPARED)
                {
                    int nSpellLevel, i, Max;
                    string sIDX, sSpellbookID, sClass = IntToString(nClass);
                    for(nSpellLevel = 0; nSpellLevel <= 9; nSpellLevel++)
                    {
                        sIDX = "SpellbookIDX" + IntToString(nSpellLevel) + "_" + sClass;
                        Max = persistant_array_get_size(oPC, sIDX);
                        for(i = 0; i < Max; i++)
                        {
                            sSpellbookID = persistant_array_get_string(oPC, sIDX, i);
                            if(sSpellbookID != "")
                            {
                                DeletePersistantLocalString(oPC, sArrayName+"_"+sSpellbookID);
                            }
                        }
                        persistant_array_delete(oPC, sIDX);
                    }
                }
                else
                {
                    persistant_array_delete(oPC, sArrayName);
                    persistant_array_create(oPC, sArrayName);
                }
            }
            //delay it so wipespellbookhidefeats has time to start to run
            //but before the deletes actually happen
            DelayCommand(0.1, SetupSpells(oPC, nClass));
        }
    }
}

//NewSpellbookSpell() helper functions
int bTargetingAllowed(int nSpellID);
void CheckPrepSlots(int nClass, int nSpellID, int nSpellbookID, int bIsAction = FALSE);
void CheckSpontSlots(int nClass, int nSpellID, int nSpellSlotLevel, int bIsAction = FALSE);
void DoCleanUp(int nMetamagic);

void CastSpontaneousSpell(int nClass, int bInstantSpell = FALSE)
{
    //get the spellbook ID
    int nFakeSpellID = GetSpellId();
    int nSpellID = GetPowerFromSpellID(nFakeSpellID);
    if(nSpellID == -1) nSpellID = 0;

    //Check the target first
    if(!bTargetingAllowed(nSpellID))
        return;

    // if OBJECT_SELF is fighting - stop fighting and cast spell
    if(GetCurrentAction() == ACTION_ATTACKOBJECT)
        ClearAllActions();

    //if its a subradial spell, get the master
    int nMasterFakeSpellID = StringToInt(Get2DACache("spells", "Master", nFakeSpellID));
    if(!nMasterFakeSpellID)
        nMasterFakeSpellID = nFakeSpellID;

    int nSpellbookID = SpellToSpellbookID(nMasterFakeSpellID);

    // Paranoia - It should not be possible to get here without having the spells available array existing
    if(!persistant_array_exists(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass)))
    {
        if(DEBUG) DoDebug("ERROR: NewSpellbookSpell: NewSpellbookMem_" + IntToString(nClass) + " array does not exist");
        persistant_array_create(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass));
    }

    int nSpellLevel = StringToInt(Get2DACache(GetFileForClass(nClass), "Level", nSpellbookID));

    // Make sure the caster has uses of this spell remaining
    // 2009-9-20: Add metamagic feat abilities. -N-S
    int nMetamagic = GetLocalInt(OBJECT_SELF, "MetamagicFeatAdjust");
    if(nMetamagic)
    {
        //Need to check if metamagic can be applied to a spell
        int nMetaTest;
        int nMetaType = HexToInt(Get2DACache("spells", "MetaMagic", nSpellID));

        int nSpellSlotLevel = nSpellLevel;
        switch(nMetamagic)
        {
            case METAMAGIC_NONE:     nMetaTest = 1; break; //no need to change anything
            case METAMAGIC_EMPOWER:  nMetaTest = nMetaType &  1; nSpellLevel += 2; break;
            case METAMAGIC_EXTEND:   nMetaTest = nMetaType &  2; nSpellLevel += 1; break;
            case METAMAGIC_MAXIMIZE: nMetaTest = nMetaType &  4; nSpellLevel += 3; break;
            case METAMAGIC_QUICKEN:  nMetaTest = nMetaType &  8; nSpellLevel += 4; break;
            case METAMAGIC_SILENT:   nMetaTest = nMetaType & 16; nSpellLevel += 1; break;
            case METAMAGIC_STILL:    nMetaTest = nMetaType & 32; nSpellLevel += 1; break;
        }

        if(!nMetaTest)//can't use selected metamagic with this spell
        {
            nMetamagic = METAMAGIC_NONE;
            ActionDoCommand(SendMessageToPC(OBJECT_SELF, "You can't use "+GetStringByStrRef(StringToInt(Get2DACache("spells", "Name", nSpellID)))+"with selected metamagic."));
            nSpellLevel = nSpellSlotLevel;
        }
        else if(nSpellLevel > 9)//now test the spell level
        {
            nMetamagic = METAMAGIC_NONE;
            ActionDoCommand(SendMessageToPC(OBJECT_SELF, "Modified spell level is to high! Casting spell without metamagic"));
            nSpellLevel = nSpellSlotLevel;
        }
        else if(GetLocalInt(OBJECT_SELF, "PRC_metamagic_state") == 1)
            SetLocalInt(OBJECT_SELF, "MetamagicFeatAdjust", 0);
    }

    CheckSpontSlots(nClass, nSpellID, nSpellLevel);
    if(GetLocalInt(OBJECT_SELF, "NSB_Cast"))
        ActionDoCommand(CheckSpontSlots(nClass, nSpellID, nSpellLevel, TRUE));
    else
        return;

    // Calculate DC. 10 + spell level on the casting class's list + DC increasing ability mod
    //int nDC = 10 + nSpellLevel + GetDCAbilityModForClass(nClass, OBJECT_SELF);
    // This is wrong and is breaking things, and is already calculated in the function it calls anyway - Strat

    //remove any old effects
    //seems cheat-casting breaks hardcoded removal
    //and cant remove effects because I dont know all the targets!
    if(!bInstantSpell)
    {
        //Handle quicken metamagic and Duskblade's Quick Cast
        if((nMetamagic & METAMAGIC_QUICKEN) || GetLocalInt(OBJECT_SELF, "QuickCast"))
        {
            //Adding Auto-Quicken III - deleted after casting has finished.
            object oSkin = GetPCSkin(OBJECT_SELF);
            int nCastDur = StringToInt(Get2DACache("spells", "ConjTime", nSpellID)) + StringToInt(Get2DACache("spells", "CastTime", nSpellID));
            itemproperty ipAutoQuicken = ItemPropertyBonusFeat(IP_CONST_NSB_AUTO_QUICKEN);
            ActionDoCommand(AddItemProperty(DURATION_TYPE_TEMPORARY, ipAutoQuicken, oSkin, nCastDur/1000.0f));
            DeleteLocalInt(OBJECT_SELF, "QuickCast");
        }
    }

    //cast the spell
    //dont need to override level, the spellscript will calculate it
    //class is read from "NSB_Class"
    ActionCastSpell(nSpellID, 0, -1, 0, nMetamagic, CLASS_TYPE_INVALID, 0, 0, OBJECT_INVALID, bInstantSpell);

    //Clean up
    ActionDoCommand(DoCleanUp(nMetamagic));
}

void CastPreparedSpell(int nClass, int nMetamagic = METAMAGIC_NONE, int bInstantSpell = FALSE)
{
    object oPC = OBJECT_SELF;

    //get the spellbook ID
    int nFakeSpellID = GetSpellId();
    int nSpellID = GetPowerFromSpellID(nFakeSpellID);
    if(nSpellID == -1) nSpellID = 0;

    //Check the target first
    if(!bTargetingAllowed(nSpellID))
        return;

    // if OBJECT_SELF is fighting - stop fighting and cast spell
    if(GetCurrentAction() == ACTION_ATTACKOBJECT)
        ClearAllActions();

    //if its a subradial spell, get the master
    int nMasterFakeSpellID = StringToInt(Get2DACache("spells", "Master", nFakeSpellID));
    if(!nMasterFakeSpellID)
        nMasterFakeSpellID = nFakeSpellID;

    int nSpellbookID = SpellToSpellbookID(nMasterFakeSpellID);

    // Paranoia - It should not be possible to get here without having the spells available array existing
    if(!persistant_array_exists(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass)))
    {
        if(DEBUG) DoDebug("ERROR: NewSpellbookSpell: NewSpellbookMem_" + IntToString(nClass) + " array does not exist");
        persistant_array_create(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass));
    }

    int nSpellLevel = StringToInt(Get2DACache(GetFileForClass(nClass), "Level", nSpellbookID));

    // Make sure the caster has uses of this spell remaining
    CheckPrepSlots(nClass, nSpellID, nSpellbookID);
    if(GetLocalInt(OBJECT_SELF, "NSB_Cast"))
        ActionDoCommand(CheckPrepSlots(nClass, nSpellID, nSpellbookID, TRUE));
    else
        return;

    // Calculate DC. 10 + spell level on the casting class's list + DC increasing ability mod
    //int nDC = 10 + nSpellLevel + GetDCAbilityModForClass(nClass, OBJECT_SELF);
    // This is wrong and is breaking things, and is already calculated in the function it calls anyway - Strat

    //remove any old effects
    //seems cheat-casting breaks hardcoded removal
    //and cant remove effects because I dont know all the targets!
    if(!bInstantSpell)
    {
        //Handle quicken metamagic and Duskblade's Quick Cast
        if((nMetamagic & METAMAGIC_QUICKEN) || GetLocalInt(OBJECT_SELF, "QuickCast"))
        {
            //Adding Auto-Quicken III - deleted after casting has finished.
            object oSkin = GetPCSkin(OBJECT_SELF);
            int nCastDur = StringToInt(Get2DACache("spells", "ConjTime", nSpellID)) + StringToInt(Get2DACache("spells", "CastTime", nSpellID));
            itemproperty ipAutoQuicken = ItemPropertyBonusFeat(IP_CONST_NSB_AUTO_QUICKEN);
            ActionDoCommand(AddItemProperty(DURATION_TYPE_TEMPORARY, ipAutoQuicken, oSkin, nCastDur/1000.0f));
            DeleteLocalInt(OBJECT_SELF, "QuickCast");
        }
        else if(nClass == CLASS_TYPE_HEALER)
        {
            if(GetHasFeat(FEAT_EFFORTLESS_HEALING)
            && GetIsOfSubschool(nSpellID, SUBSCHOOL_HEALING))
            {
                object oSkin = GetPCSkin(OBJECT_SELF);
                //all spells from healing subschool except Close Wounds have casting time of 2.5s
                float fCastDur = nSpellID == SPELL_CLOSE_WOUNDS ? 1.0f : 2.5f;
                itemproperty ipImpCombatCast = ItemPropertyBonusFeat(IP_CONST_NSB_IMP_COMBAT_CAST);
                ActionDoCommand(AddItemProperty(DURATION_TYPE_TEMPORARY, ipImpCombatCast, oSkin, fCastDur));
            }
        }
    }

    //cast the spell
    //dont need to override level, the spellscript will calculate it
    //class is read from "NSB_Class"
    ActionCastSpell(nSpellID, 0, -1, 0, nMetamagic, CLASS_TYPE_INVALID, 0, 0, OBJECT_INVALID, bInstantSpell);

    //Clean up
    ActionDoCommand(DoCleanUp(nMetamagic));
}

void NewSpellbookSpell(int nClass, int nSpellbookType, int nMetamagic = METAMAGIC_NONE, int bInstantSpell = FALSE)
{
    object oPC = OBJECT_SELF;

    // if oPC is fighting - stop fighting and cast spell
    if(GetCurrentAction(oPC) == ACTION_ATTACKOBJECT)
        ClearAllActions();

    //get the spellbook ID
    int nFakeSpellID = GetSpellId();
    int nSpellID = GetPowerFromSpellID(nFakeSpellID);
    if(nSpellID == -1) nSpellID = 0;

    //Check the target first
    if(!bTargetingAllowed(nSpellID))
        return;

    //if its a subradial spell, get the master
    int nMasterFakeSpellID = StringToInt(Get2DACache("spells", "Master", nFakeSpellID));
    if(!nMasterFakeSpellID)
        nMasterFakeSpellID = nFakeSpellID;

    int nSpellbookID = SpellToSpellbookID(nMasterFakeSpellID);

    // Paranoia - It should not be possible to get here without having the spells available array existing
    if(!persistant_array_exists(oPC, "NewSpellbookMem_" + IntToString(nClass)))
    {
        if(DEBUG) DoDebug("ERROR: NewSpellbookSpell: NewSpellbookMem_" + IntToString(nClass) + " array does not exist");
        persistant_array_create(oPC, "NewSpellbookMem_" + IntToString(nClass));
    }

    string sFile = GetFileForClass(nClass);
    int nSpellLevel = StringToInt(Get2DACache(sFile, "Level", nSpellbookID));

    // Make sure the caster has uses of this spell remaining
    // 2009-9-20: Add metamagic feat abilities. -N-S
    if(nSpellbookType == SPELLBOOK_TYPE_PREPARED)
    {
        CheckPrepSlots(nClass, nSpellID, nSpellbookID);
        if(GetLocalInt(oPC, "NSB_Cast"))
            ActionDoCommand(CheckPrepSlots(nClass, nSpellID, nSpellbookID, TRUE));
        else
            return;
    }
    else if(nSpellbookType == SPELLBOOK_TYPE_SPONTANEOUS)
    {
        nMetamagic = GetLocalInt(oPC, "MetamagicFeatAdjust");
        if(nMetamagic)
        {
            //Need to check if metamagic can be applied to a spell
            int nMetaTest;
            int nMetaType = HexToInt(Get2DACache("spells", "MetaMagic", nSpellID));

            int nSpellSlotLevel = nSpellLevel;
            switch(nMetamagic)
            {
                case METAMAGIC_NONE:     nMetaTest = 1; break; //no need to change anything
                case METAMAGIC_EMPOWER:  nMetaTest = nMetaType &  1; nSpellLevel += 2; break;
                case METAMAGIC_EXTEND:   nMetaTest = nMetaType &  2; nSpellLevel += 1; break;
                case METAMAGIC_MAXIMIZE: nMetaTest = nMetaType &  4; nSpellLevel += 3; break;
                case METAMAGIC_QUICKEN:  nMetaTest = nMetaType &  8; nSpellLevel += 4; break;
                case METAMAGIC_SILENT:   nMetaTest = nMetaType & 16; nSpellLevel += 1; break;
                case METAMAGIC_STILL:    nMetaTest = nMetaType & 32; nSpellLevel += 1; break;
            }

            if(!nMetaTest)//can't use selected metamagic with this spell
            {
                nMetamagic = METAMAGIC_NONE;
                ActionDoCommand(SendMessageToPC(oPC, "You can't use "+GetStringByStrRef(StringToInt(Get2DACache("spells", "Name", nSpellID)))+"with selected metamagic."));
                nSpellLevel = nSpellSlotLevel;
            }
            else if(nSpellLevel > 9)//now test the spell level
            {
                nMetamagic = METAMAGIC_NONE;
                ActionDoCommand(SendMessageToPC(oPC, "Modified spell level is to high! Casting spell without metamagic"));
                nSpellLevel = nSpellSlotLevel;
            }
            else if(GetLocalInt(oPC, "PRC_metamagic_state") == 1)
                SetLocalInt(oPC, "MetamagicFeatAdjust", 0);
        }

        CheckSpontSlots(nClass, nSpellID, nSpellLevel);
        if(GetLocalInt(oPC, "NSB_Cast"))
            ActionDoCommand(CheckSpontSlots(nClass, nSpellID, nSpellLevel, TRUE));
        else
            return;
    }

    // Calculate DC. 10 + spell level on the casting class's list + DC increasing ability mod
    //int nDC = 10 + nSpellLevel + GetDCAbilityModForClass(nClass, OBJECT_SELF);
    // This is wrong and is breaking things, and is already calculated in the function it calls anyway - Strat

    //remove any old effects
    //seems cheat-casting breaks hardcoded removal
    //and cant remove effects because I dont know all the targets!
    if(!bInstantSpell)
    {
        //Handle quicken metamagic and Duskblade's Quick Cast
        if((nMetamagic & METAMAGIC_QUICKEN) || GetLocalInt(oPC, "QuickCast"))
        {
            //Adding Auto-Quicken III - deleted after casting has finished.
            object oSkin = GetPCSkin(oPC);
            int nCastDur = StringToInt(Get2DACache("spells", "ConjTime", nSpellID)) + StringToInt(Get2DACache("spells", "CastTime", nSpellID));
            itemproperty ipAutoQuicken = ItemPropertyBonusFeat(IP_CONST_NSB_AUTO_QUICKEN);
            ActionDoCommand(AddItemProperty(DURATION_TYPE_TEMPORARY, ipAutoQuicken, oSkin, nCastDur/1000.0f));
            DeleteLocalInt(oPC, "QuickCast");
        }
        else if(nClass == CLASS_TYPE_HEALER)
        {
            if(GetHasFeat(FEAT_EFFORTLESS_HEALING)
            && GetIsOfSubschool(nSpellID, SUBSCHOOL_HEALING))
            {
                object oSkin = GetPCSkin(oPC);
                //all spells from healing subschool except Close Wounds have casting time of 2.5s
                float fCastDur = nSpellID == SPELL_CLOSE_WOUNDS ? 1.0f : 2.5f;
                itemproperty ipImpCombatCast = ItemPropertyBonusFeat(IP_CONST_NSB_IMP_COMBAT_CAST);
                ActionDoCommand(AddItemProperty(DURATION_TYPE_TEMPORARY, ipImpCombatCast, oSkin, fCastDur));
            }
        }
    }

    //cast the spell
    //dont need to override level, the spellscript will calculate it
    //class is read from "NSB_Class"
    ActionCastSpell(nSpellID, 0, -1, 0, nMetamagic, CLASS_TYPE_INVALID, 0, 0, OBJECT_INVALID, bInstantSpell);

    //Clean up
    ActionDoCommand(DoCleanUp(nMetamagic));
}

int bTargetingAllowed(int nSpellID)
{
    object oTarget = GetSpellTargetObject();
    if(GetIsObjectValid(oTarget))
    {
        int nTargetType = ~(HexToInt(Get2DACache("spells", "TargetType", nSpellID)));

        //test targetting self
        if(oTarget == OBJECT_SELF)
        {
            if(nTargetType & 1)
            {
                if(DEBUG) DoDebug("bTargetingAllowed: You cannot target yourself.");
                return FALSE;
            }
        }
        //test targetting others
        else if(GetObjectType(oTarget) == OBJECT_TYPE_CREATURE)
        {
            if(nTargetType & 2)
            {
                if(DEBUG) DoDebug("bTargetingAllowed: You cannot target creatures.");
                return FALSE;
            }
        }
    }
    return TRUE;
}

void CheckPrepSlots(int nClass, int nSpellID, int nSpellbookID, int bIsAction = FALSE)
{
    DeleteLocalInt(OBJECT_SELF, "NSB_Cast");
    int nCount = persistant_array_get_int(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass), nSpellbookID);
    if(DEBUG) DoDebug("NewSpellbookSpell: NewSpellbookMem_" + IntToString(nClass) + "[" + IntToString(nSpellbookID) + "] = " + IntToString(nCount));
    if(nCount < 1)
    {
        string sSpellName = GetStringByStrRef(StringToInt(Get2DACache("spells", "Name", nSpellID)));
        // "You have no castings of " + sSpellName + " remaining"
        string sMessage   = ReplaceChars(GetStringByStrRef(16828411), "<spellname>", sSpellName);

        FloatingTextStringOnCreature(sMessage, OBJECT_SELF, FALSE);
        if(bIsAction)
            ClearAllActions();
    }
    else
    {
        SetLocalInt(OBJECT_SELF, "NSB_Cast", 1);
        if(bIsAction)
        {
            SetLocalInt(OBJECT_SELF, "NSB_Class", nClass);
            SetLocalInt(OBJECT_SELF, "NSB_SpellbookID", nSpellbookID);
        }
    }
}

void CheckSpontSlots(int nClass, int nSpellID, int nSpellSlotLevel, int bIsAction = FALSE)
{
    DeleteLocalInt(OBJECT_SELF, "NSB_Cast");
    int nCount = persistant_array_get_int(OBJECT_SELF, "NewSpellbookMem_" + IntToString(nClass), nSpellSlotLevel);
    if(DEBUG) DoDebug("NewSpellbookSpell: NewSpellbookMem_" + IntToString(nClass) + "[" + IntToString(nSpellSlotLevel) + "] = " + IntToString(nCount));
    if(nCount < 1)
    {
        // "You have no castings of spells of level " + IntToString(nSpellLevel) + " remaining"
        string sMessage   = ReplaceChars(GetStringByStrRef(16828409), "<spelllevel>", IntToString(nSpellSlotLevel));
        FloatingTextStringOnCreature(sMessage, OBJECT_SELF, FALSE);
        if(bIsAction)
            ClearAllActions();
    }
    else
    {
        SetLocalInt(OBJECT_SELF, "NSB_Cast", 1);
        if(bIsAction)
        {
            SetLocalInt(OBJECT_SELF, "NSB_Class", nClass);
            SetLocalInt(OBJECT_SELF, "NSB_SpellLevel", nSpellSlotLevel);
        }
    }
}

void DoCleanUp(int nMetamagic)
{
    if(nMetamagic & METAMAGIC_QUICKEN)
    {
        object oSkin = GetPCSkin(OBJECT_SELF);
        RemoveItemProperty(oSkin, ItemPropertyBonusFeat(IP_CONST_NSB_AUTO_QUICKEN));
    }
    DeleteLocalInt(OBJECT_SELF, "NSB_Class");
    DeleteLocalInt(OBJECT_SELF, "NSB_SpellLevel");
    DeleteLocalInt(OBJECT_SELF, "NSB_SpellbookID");
}