/**
 * convoCC misc functions
 */

#include "inc_utility"
#include "prc_alterations"
#include "inc_letoscript"
#include "inc_letocommands"
#include "inc_dynconv"
#include "prc_ccc_const"
#include "inc_sql"

// checks if it's a multiplayer game and that letoscript is set up correctly
// returns 0 for pass and 1 for fail
int DoLetoscriptTest(object oPC);

// makes a string based on the PC name and player public CD key
string Encrypt(object oPC);

// removes all equipped items from the PC
// that means ALL items, even plot ones
void DoStripPC(object oPC);

// checks if oPC is valid and in an area then boots them
void CheckAndBoot(object oPC);

// 'nice' version of CheckAndBoot() that gives a warning and a 5 second countdown
void CheckAndBootNicely(object oPC);

/**
 * main cutscene function
 * letoscript changes to the PC clone are done via this function
 * nSetup indicates whether the cutscene needs seting up or not
 */
void DoCutscene(object oPC, int nSetup = FALSE);

/**
 * Cutscene pseudo HB functions
 */
 
// used to cleanup clones when a player leaves
void CloneMasterCheck();

// sets up the camera to rotate around the clone 
// and the clone do random animations
void DoRotatingCamera(object oPC);

/**
 * functions to set appearance, portrait, soundset
 */
 
// sets race appearance as defined in racialtype.2da
// removes wings, tails and undead graft arm as well as making invisible bits visible
void DoSetRaceAppearance(object oPC);
 
// assigns the ccc chosen gender to the clone and resets the soundset
// if it's changed
void DoCloneGender(object oPC);

// changes the appearance of the PC and clone
void DoSetAppearance(object oPC);

// deletes local variables stored on the PC before the player is booted to make the new character
void DoCleanup();

// set up the ability choice in "<statvalue> (racial <+/-modifier>) <statname>. Cost to increase <cost>" format
void AddAbilityChoice(int nStatValue, string sStatName, string sRacialAdjust, int nAbilityConst);

// subtracts the correct amount of points for increasing the current ability score
// returns the current ability score incremented by 1
int IncreaseAbilityScore(int nCurrentAbilityScore);

//this returns the cost to get to a score
//or the cost saved by dropping from that score
int GetCost(int nAbilityScore);

// this marks if the PC qualifies for skill focus feats that are class specific
void MarkSkillFocusPrereq(int nSkill, string sVarName);

// works out the next stage to go to between STAGE_FEAT_CHECK and STAGE_APPEARANCE
// when given the stage just completed
// covers caster and familiar related choices
int GetNextCCCStage(int nStage, int nSpellCasterStage = TRUE);

// adds a reply choice to the cache array
// used to store a set of dynamic conversation choices so they don't have to be
// unnecessarily regenerated from the database
void AddCachedChoice(string sText, int nValue, object oPC = OBJECT_SELF);

// delete the stored convo choice cache array
void ClearCachedChoices(object oPC = OBJECT_SELF);

// sets the convoCC's current choice list to the values stored in the cache array 
void AddChoicesFromCache(object oPC = OBJECT_SELF);

// checks if the PC has the two feats given as arguements
// '****' as an arguement is treated as an automatic TRUE for that arguement
// used to check if the PC meets the prerequisites in the PreReqFeat1 AND PreReqFeat2
// columns of feat.2da
int GetMeetsANDPreReq(string sPreReqFeat1, string sPreReqFeat2);

// checks if the PC has any one of the 5 feats given as arguements
// '****' as an arguement is treated as an automatic TRUE for that arguement
// used to check if the PC meets the prerequisites in the OrReqFeat0 OR OrReqFeat1 
// OR OrReqFeat2 OR OrReqFeat3 OR OrReqFeat4 columns of feat.2da
int GetMeetsORPreReq(string sOrReqFeat0, string sOrReqFeat1, string sOrReqFeat2, string sOrReqFeat3, string sOrReqFeat4);

// loops through the feat array on the PC to see if it has nReqFeat
int PreReqFeatArrayLoop(int nReqFeat);

// checks if the PC has enough skill ranks in the two skills
// '****' as ReqSkill arguement is treated as an automatic TRUE for that arguement
// used to check if the PC meets the prerequisites in the ReqSkill AND ReqSkill2
// columns of feat.2da
int GetMeetSkillPrereq(string sReqSkill, string sReqSkill2, string sReqSkillRanks,  string sReqSkillRanks2);

// checks if the PC has enough points in sReqSkill
int CheckSkillPrereq(string sReqSkill, string sReqSkillRanks);

// adds all the cantrips to a wizard's spellbook
void SetWizCantrips(int iSpellschool);

// loops through the spell array on the PC to see if they already know the spell
// returns TRUE if they know it
int GetIsSpellKnown(int nSpell, int nSpellLevel);

// sets up the appearance choices if the PRC_CONVOCC_USE_RACIAL_APPEARANCES switch is set
void SetupRacialAppearances();

// adds appearance choices on being passed an APPEARANCE_TYPE_* constant
void AddAppearanceChoice(int nType, int nOnlyChoice = FALSE);

// adds the head choices based on race and gender
void SetupHeadChoices();

// maps the appearance to a bioware playable race
int MapAppearanceToRace(int nAppearance);

/* 2da cache functions */

// loops through a 2da, using the cache
// s2da is the name of the 2da, sColumnName the column to read the values
// from, nFileEnd the number of lines in the 2da
void Do2daLoop(string s2da, string sColumnName, int nFileEnd);

// loops through racialtypes.2da
void DoRacialtypesLoop();

// loops through classes.2da
void DoClassesLoop();

// loops through skills.2da
void DoSkillsLoop();

// loops through feat.2da
void DoFeatLoop(int nClassFeatStage = FALSE);

// loops through cls_feat_***.2da
void DoBonusFeatLoop();

// loops through spells.2da
void DoSpellsLoop(int nStage);

// loops through domains.2da
void  DoDomainsLoop();

// loops through appearance.2da
void DoAppearanceLoop();

// loops through portraits.2da
void DoPortraitsLoop(int nGenderSort = TRUE);

// loops through soundset.2da
void DoSoundsetLoop(int nGenderSort = TRUE);

// loops through wingmodel.2da
void DoWingmodelLoop();

// loops through tailmodel.2da
void DoTailmodelLoop();

// stores the feats found in race_feat_***.2da as an array on the PC
void AddRaceFeats(int nRace);

// stores the feats found in cls_feat_***.2da in the feat array on the PC
void AddClassFeats(int nClass);

// stores the feat listed in domais.2da in the feat array on the PC
void AddDomainFeats();

// loops through colours.2da depending on the stage for which column it uses.
void AddColourChoices(int nStage, int nCategory);

/* function definitions */

int DoLetoscriptTest(object oPC)
{
    int bBoot;
    //check that its a multiplayer game
    if(GetPCPublicCDKey(oPC) == "")
    {
        SendMessageToPC(oPC, "This module must be hosted as a multiplayer game with NWNX and Letoscript");
        WriteTimestampedLogEntry("This module must be hosted as a multiplayer game with NWNX and Letoscript");
        bBoot = TRUE;
    }

    //check that letoscript is setup correctly
    string sScript;
    if(GetPRCSwitch(PRC_LETOSCRIPT_PHEONIX_SYNTAX))
        sScript = LetoGet("FirstName")+" "+LetoGet("LastName");
    else
        sScript = LetoGet("FirstName")+"print ' ';"+LetoGet("LastName");
        
    StackedLetoScript(sScript);
    RunStackedLetoScriptOnObject(oPC, "LETOTEST", "SCRIPT", "", FALSE);
    string sResult = GetLocalString(GetModule(), "LetoResult");
    string sName = GetName(oPC);
    if((    sResult != sName
         && sResult != sName+" "
         && sResult != " "+sName)
         )
    {
        SendMessageToPC(oPC, "Error: Letoscript is not setup correctly or it cannot find your bic file. Check nwnx_leto.log for error messages.");
        WriteTimestampedLogEntry("Error: Letoscript is not setup correctly or it cannot find your bic file. Check nwnx_leto.log for error messages.");
        bBoot = TRUE;
    }
    
    return bBoot;
}

string Encrypt(object oPC)
{
    string sName = GetName(oPC);
    int nKey = GetPRCSwitch(PRC_CONVOCC_ENCRYPTION_KEY);
    if(nKey == 0)
        nKey = 10;
    string sReturn;

    string sPublicCDKey = GetPCPublicCDKey(oPC);
    int nKeyTotal;
    int i;
    for(i=1;i<GetStringLength(sPublicCDKey);i++)
    {
        nKeyTotal += StringToInt(GetStringLeft(GetStringRight(sPublicCDKey, i),1));
    }
    sReturn = IntToString(nKeyTotal);
    return sReturn;
}

void DoStripPC(object oPC)
{
    // remove equipped items
    int i;
    for(i=0; i<NUM_INVENTORY_SLOTS; i++)
    {
        object oEquip = GetItemInSlot(i,oPC);
        if(GetIsObjectValid(oEquip))
        {
            SetPlotFlag(oEquip,FALSE);
            DestroyObject(oEquip);
        }
    }
    // empty inventory
    object oItem = GetFirstItemInInventory(oPC);
    while(GetIsObjectValid(oItem))
    {
        SetPlotFlag(oItem,FALSE);
        DestroyObject(oItem);
        oItem = GetNextItemInInventory(oPC);
    }
}

void CheckAndBoot(object oPC)
{
    if(GetIsObjectValid(GetAreaFromLocation(GetLocation(oPC))))
        BootPC(oPC);
}

void CheckAndBootNicely(object oPC)
{
    // Give a "helpful" message
    string sMessage = GetLocalString(oPC, "CONVOCC_ENTER_BOOT_MESSAGE");
    if (sMessage == "") // yes, you should have given your own message here
        sMessage = "Eeek! Something's not right.";
    // no avoiding the convoCC, so stop them running off
    effect eParal = EffectCutsceneImmobilize();
    eParal = SupernaturalEffect(eParal);
    ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eParal, oPC, 9999.9);
    // floaty text info as we can't use the dynamic convo for this
    DelayCommand(10.0, FloatingTextStringOnCreature(sMessage, oPC, FALSE));
    DelayCommand(11.0, FloatingTextStringOnCreature("You will be booted in 5...", oPC, FALSE));
    DelayCommand(12.0, FloatingTextStringOnCreature("4...", oPC, FALSE));
    DelayCommand(13.0, FloatingTextStringOnCreature("3...", oPC, FALSE));
    DelayCommand(14.0, FloatingTextStringOnCreature("2...", oPC, FALSE));
    DelayCommand(15.0, FloatingTextStringOnCreature("1...", oPC, FALSE));
    AssignCommand(oPC, DelayCommand(16.0, CheckAndBoot(oPC)));
}

void DoSetRaceAppearance(object oPC)
{
    if(DEBUG) DoDebug(DebugObject2Str(oPC));
    // sets the appearance type
    int nSex = GetLocalInt(oPC, "Gender");
    int nRace = GetLocalInt(oPC, "Race");
    // appearance type switches go here
    if(nRace == RACIAL_TYPE_RAKSHASA || nRace == RACIAL_TYPE_ZAKYA_RAKSHASA || nRace == RACIAL_TYPE_NAZTHARUNE_RAKSHASA
        && nSex == GENDER_FEMALE
        && GetPRCSwitch(PRC_CONVOCC_RAKSHASA_FEMALE_APPEARANCE))
        SetCreatureAppearanceType(oPC, APPEARANCE_TYPE_RAKSHASA_TIGER_FEMALE);
    else
        SetCreatureAppearanceType(oPC, 
                    StringToInt(Get2DACache("racialtypes", "Appearance",
                        GetLocalInt(oPC, "Race"))));
    
    // remove wings and tails - so this can be set later
    // enforcing wing and tail related switches comes later too
    SetCreatureWingType(CREATURE_WING_TYPE_NONE);
    SetCreatureTailType(CREATURE_TAIL_TYPE_NONE);
    
    // get rid of invisible/undead etc bits, but keep tatoos as is
    SetCreatureBodyPart(CREATURE_PART_RIGHT_FOOT, 1);
    SetCreatureBodyPart(CREATURE_PART_LEFT_FOOT, 1);
    SetCreatureBodyPart(CREATURE_PART_PELVIS, 1);
    SetCreatureBodyPart(CREATURE_PART_BELT, 0);
    SetCreatureBodyPart(CREATURE_PART_NECK, 1);
    SetCreatureBodyPart(CREATURE_PART_RIGHT_SHOULDER, 0);
    SetCreatureBodyPart(CREATURE_PART_LEFT_SHOULDER, 0);
    SetCreatureBodyPart(CREATURE_PART_RIGHT_HAND, 1);
    SetCreatureBodyPart(CREATURE_PART_LEFT_HAND, 1);
    // make invisible heads visible, otherwise leave
    if(GetCreatureBodyPart(CREATURE_PART_HEAD) == 0)
        SetCreatureBodyPart(CREATURE_PART_HEAD, 1);
    // preserve tattoos, get rid of anything else
    if(!(GetCreatureBodyPart(CREATURE_PART_RIGHT_SHIN) == 1))
        SetCreatureBodyPart(CREATURE_PART_RIGHT_SHIN, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_LEFT_SHIN) == 1))
        SetCreatureBodyPart(CREATURE_PART_LEFT_SHIN, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_RIGHT_THIGH) == 1))
        SetCreatureBodyPart(CREATURE_PART_RIGHT_THIGH, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_LEFT_THIGH) == 1))
        SetCreatureBodyPart(CREATURE_PART_LEFT_THIGH, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_RIGHT_FOREARM) == 1))
        SetCreatureBodyPart(CREATURE_PART_RIGHT_FOREARM, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_LEFT_FOREARM) == 1))
        SetCreatureBodyPart(CREATURE_PART_LEFT_FOREARM, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_RIGHT_BICEP) == 1))
        SetCreatureBodyPart(CREATURE_PART_RIGHT_BICEP, 2);
    if(!(GetCreatureBodyPart(CREATURE_PART_RIGHT_BICEP) == 1))
        SetCreatureBodyPart(CREATURE_PART_RIGHT_BICEP, 2);
}

void DoCloneGender(object oPC)
{
    object oClone = GetLocalObject(oPC, "Clone");
    if(!GetIsObjectValid(oClone))
        return;
    int nSex = GetLocalInt(oPC, "Gender");
    int nCurrentSex = GetGender(oClone);
    StackedLetoScript(LetoSet("Gender", IntToString(nSex), "byte"));
    // if the gender needs changing, reset the soundset
    if (nSex != nCurrentSex)
        StackedLetoScript(LetoSet("SoundSetFile", IntToString(0), "word"));
    string sResult;
    RunStackedLetoScriptOnObject(oClone, "OBJECT", "SPAWN", "prc_ccc_app_lspw", TRUE);
    sResult = GetLocalString(GetModule(), "LetoResult");
    SetLocalObject(GetModule(), "PCForThread"+sResult, oPC);
}

void DoSetAppearance(object oPC)
{
    if(DEBUG) DoDebug(DebugObject2Str(oPC));
    // get the appearance type
    int nAppearance = GetLocalInt(oPC, "Appearance");
    // get the clone object
    object oClone = GetLocalObject(oPC, "Clone");
    SetCreatureAppearanceType(oClone, nAppearance);
}

void DoCutscene(object oPC, int nSetup = FALSE)
{
    string sScript;
    int nStage = GetStage(oPC);
    
    // check the PC has finished entering the area
    if(!GetIsObjectValid(GetArea(oPC)))
    {
        DelayCommand(1.0, DoCutscene(oPC, nSetup));
        return;
    }
    
    if(DEBUG) DoDebug("DoCutscene() stage is :" + IntToString(nStage) + " nSetup = " + IntToString(nSetup));
    
    if (nStage >= STAGE_RACE_CHECK) // if we don't need to set the clone up
    {
        object oClone;
        
        if(nStage == STAGE_RACE_CHECK || (nStage > STAGE_RACE_CHECK && nSetup))
        {
            // make the PC look like the race they have chosen
            DoSetRaceAppearance(oPC);
            // clone the PC and hide the swap with a special effect
            // make the real PC non-collideable
            effect eGhost = EffectCutsceneGhost();
            ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eGhost, oPC, 99999999.9);
            // make the swap and hide with an effect
            effect eVis = EffectVisualEffect(VFX_FNF_SUMMON_MONSTER_1);
            ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eVis, GetLocation(oPC));
            // make clone
            oClone = CopyObject(oPC, GetLocation(oPC), OBJECT_INVALID, "PlayerClone");
            ChangeToStandardFaction(oClone, STANDARD_FACTION_MERCHANT);
            // make the real PC invisible
            effect eInvis = EffectVisualEffect(VFX_DUR_CUTSCENE_INVISIBILITY);
            ApplyEffectToObject(DURATION_TYPE_TEMPORARY, eInvis, oPC, 9999.9);
            // swap local objects
            SetLocalObject(oPC, "Clone", oClone);
            SetLocalObject(oClone, "Master", oPC);
            // this makes sure the clone gets destroyed if the PC leaves the game
            AssignCommand(oClone, CloneMasterCheck());
            // end of clone making
            
            int nGender = GetLocalInt(oPC, "Gender");
            // this only needs doing if the gender has changed
            if (GetGender(oPC) != nGender)
            {
                sScript = LetoSet("Gender", IntToString(nGender), "byte");
                // reset soundset only if we've not changed it yet
                if (nStage < STAGE_SOUNDSET)
                    sScript += LetoSet("SoundSetFile", IntToString(0), "word");
            }
        }
        
        if(nStage == STAGE_APPEARANCE || (nStage > STAGE_APPEARANCE && nSetup))
        {
            DoSetAppearance(oPC);
        }
        
        if(nStage == STAGE_SOUNDSET || (nStage > STAGE_SOUNDSET && nSetup))
        {
            int nSoundset = GetLocalInt(oPC, "Soundset");
            if (nSoundset != -1) // then it has been changed
            {
                sScript += LetoSet("SoundSetFile", IntToString(nSoundset), "word");
            }
        }
        
        if (nStage == STAGE_SKIN_COLOUR_CHOICE || (nStage > STAGE_SKIN_COLOUR_CHOICE && nSetup))
        {
            int nSkin = GetLocalInt(oPC, "Skin");
            if (nSkin != -1) // then it has been changed
            {
                SetColor(oClone, COLOR_CHANNEL_SKIN, nSkin);
                //sScript += SetSkinColor(nSkin);
            }
        }
        
        if (nStage == STAGE_HAIR_COLOUR_CHOICE || (nStage > STAGE_HAIR_COLOUR_CHOICE && nSetup))
        {
            int nHair = GetLocalInt(oPC, "Hair");
            if (nHair != -1) // then it has been changed
            {
                SetColor(oClone, COLOR_CHANNEL_HAIR, nHair);
                //sScript += SetHairColor(nHair);
            }
        }
        
        if (nStage == STAGE_TATTOO1_COLOUR_CHOICE || (nStage > STAGE_TATTOO1_COLOUR_CHOICE && nSetup))
        {
            int nTattooColour1 = GetLocalInt(oPC, "TattooColour1");
            if (nTattooColour1 != -1) // then it has been changed
            {
                SetColor(oClone, COLOR_CHANNEL_TATTOO_1, nTattooColour1);
                //sScript += SetTattooColor(nTattooColour1, 1);
            }
        }
        
        if (nStage == STAGE_TATTOO2_COLOUR_CHOICE || (nStage > STAGE_TATTOO2_COLOUR_CHOICE && nSetup))
        {
            int nTattooColour2 = GetLocalInt(oPC, "TattooColour2");
            if (nTattooColour2 != -1) // then it has been changed
            {
                SetColor(oClone, COLOR_CHANNEL_TATTOO_2, nTattooColour2);
                //sScript += SetTattooColor(nTattooColour2, 2);
            }
        }
        // no point in running the letoscript commands if no changes are made
        if (sScript != "")
        {
            StackedLetoScript(sScript);
            string sResult;
            if (oClone == OBJECT_INVALID)
                oClone = GetLocalObject(oPC, "Clone");
            RunStackedLetoScriptOnObject(oClone, "OBJECT", "SPAWN", "prc_ccc_app_lspw", TRUE);
            sResult = GetLocalString(GetModule(), "LetoResult");
            SetLocalObject(GetModule(), "PCForThread"+sResult, OBJECT_SELF);
        }
    }
    // DoRotatingCamera(oPC);
}

void CloneMasterCheck()
{
    object oMaster = GetLocalObject(OBJECT_SELF, "Master");
    if(!GetIsObjectValid(oMaster))
    {
        // free up the convoCC if they logged out
        DeleteLocalObject(GetModule(), "ccc_active_pc");
        SetIsDestroyable(TRUE);
        DestroyObject(OBJECT_SELF);
    }
    else
        DelayCommand(10.0, CloneMasterCheck());
}

void DoRotatingCamera(object oPC)
{
    if (DEBUG) DoDebug("Running DoRotatingCamera()");
    if(!GetIsObjectValid(oPC))
    {
        // then the ccc is free to use again
        DeleteLocalInt(GetModule(), "ccc_active_pc");
        if (DEBUG) DoDebug("Invalid PC given to DoRotatingCamera()");
        return;
    }
    if(GetLocalInt(oPC, "StopRotatingCamera"))
    {
        DeleteLocalInt(oPC, "StopRotatingCamera");
        DeleteLocalFloat(oPC, "DoRotatingCamera");
        return;
    }
    float fDirection = GetLocalFloat(oPC, "DoRotatingCamera");
    fDirection += 30.0;
    if(fDirection > 360.0)
        fDirection -= 360.0;
    if(fDirection <= 0.0)
        fDirection += 360.0;
    SetLocalFloat(oPC, "DoRotatingCamera", fDirection);
    SetCameraMode(oPC, CAMERA_MODE_TOP_DOWN);
    SetCameraFacing(fDirection, 2.0, 45.0, CAMERA_TRANSITION_TYPE_VERY_SLOW);
    DelayCommand(6.0, DoRotatingCamera(oPC));
    //its the clone not the PC that does things
    object oClone = GetLocalObject(oPC, "Clone");
    if(GetIsObjectValid(oClone))
        oPC = oClone;
    if(d2()==1)
        AssignCommand(oPC, ActionPlayAnimation(100+Random(17)));
    else
        AssignCommand(oPC, ActionPlayAnimation(100+Random(21), 1.0, 6.0));
}

void DoCleanup()
{
    object oPC = OBJECT_SELF;
    // go through the ones used to make the character
    // delete some ints
    DeleteLocalInt(oPC, "Str");
    DeleteLocalInt(oPC, "Dex");
    DeleteLocalInt(oPC, "Con");
    DeleteLocalInt(oPC, "Int");
    DeleteLocalInt(oPC, "Wis");
    DeleteLocalInt(oPC, "Cha");

    DeleteLocalInt(oPC, "Race");

    DeleteLocalInt(oPC, "Class");
    DeleteLocalInt(oPC, "HitPoints");

    DeleteLocalInt(oPC, "Gender");

    DeleteLocalInt(oPC, "LawfulChaotic");
    DeleteLocalInt(oPC, "GoodEvil");

    DeleteLocalInt(oPC, "Familiar");
    DeleteLocalInt(oPC, "Companion");

    DeleteLocalInt(oPC, "Domain1");
    DeleteLocalInt(oPC, "Domain2");

    DeleteLocalInt(oPC, "School");
    
    DeleteLocalInt(oPC, "SpellsPerDay0");
    DeleteLocalInt(oPC, "SpellsPerDay1");
    
    DeleteLocalInt(oPC, "Soundset");
    DeleteLocalInt(oPC, "Skin");
    DeleteLocalInt(oPC, "Hair");
    DeleteLocalInt(oPC, "TattooColour1");
    DeleteLocalInt(oPC, "TattooColour2");
    
    // delete some arrays
    array_delete(oPC, "spellLvl0");
    array_delete(oPC, "spellLvl1");
    array_delete(oPC, "Feats");
    array_delete(oPC, "Skills");
}


void AddAbilityChoice(int nAbilityScore, string sAbilityName, string sRacialAdjust, int nAbilityConst)
{
    // if it is still possible to increase the ability score, add that choice
    if (nAbilityScore < GetLocalInt(OBJECT_SELF, "MaxStat") && GetLocalInt(OBJECT_SELF, "Points") >= GetCost(nAbilityScore + 1))
    {
        AddChoice(sAbilityName + " " + IntToString(nAbilityScore) + " (Racial " + sRacialAdjust + "). " 
            + GetStringByStrRef(137) + " " + IntToString(GetCost(nAbilityScore + 1)), nAbilityConst);
    }
}

int IncreaseAbilityScore(int nCurrentAbilityScore)
{
    int nPoints = GetLocalInt(OBJECT_SELF, "Points");
    // get cost and remove from total
    // note: because of how GetCost() works, the ability score is incremented here not later
    nPoints -= GetCost(++nCurrentAbilityScore);
    // store the total points left on the PC
    SetLocalInt(OBJECT_SELF, "Points", nPoints);
    return nCurrentAbilityScore;
}

int GetCost(int nAbilityScore)
{
    int nCost = (nAbilityScore-11)/2;
    if(nCost < 1)
        nCost = 1;
    return nCost;
}

void MarkSkillFocusPrereq(int nSkill, string sVarName)
{
    string sFile = GetStringLowerCase(Get2DACache("classes", "SkillsTable", GetLocalInt(OBJECT_SELF, "Class")));
    // query to see if nSkill is on the cls_skill*** list
    string sSkill = IntToString(nSkill);
    string sSQL = "SELECT data FROM prc_cached2da WHERE file = '" + sFile + "' AND columnid= 'skillindex' AND " 
    +"data = '" + sSkill + "'";
    
    PRC_SQLExecDirect(sSQL);
    if (PRC_SQLFetch() == PRC_SQL_SUCCESS)
        SetLocalInt(OBJECT_SELF, sVarName, TRUE);
}

int GetNextCCCStage(int nStage, int nSpellCasterStage = TRUE)
{
    // check we're in the right place
    if (nStage < STAGE_FEAT_CHECK)
        return -1; // sent here too early
    // get some info
    int nClass = GetLocalInt(OBJECT_SELF, "Class");
    if(nSpellCasterStage)
    {
        switch (nStage) // with no breaks to go all the way through
        {
            case STAGE_FEAT_CHECK: {
                if(nClass == CLASS_TYPE_WIZARD)
                {
                    return STAGE_WIZ_SCHOOL;
                }
            }
            case STAGE_WIZ_SCHOOL_CHECK: {
                if (nClass == CLASS_TYPE_SORCERER || nClass == CLASS_TYPE_BARD)
                {
                    return STAGE_SPELLS_0;
                }
                else if (nClass == CLASS_TYPE_WIZARD)
                {
                    return STAGE_SPELLS_1;
                }
            }
            case STAGE_SPELLS_0: {
                if (nClass == CLASS_TYPE_SORCERER || nClass == CLASS_TYPE_BARD)
                {
                    string sSpkn = Get2DACache("classes", "SpellKnownTable", nClass);
                    // if they can pick level 1 spells
                    if (StringToInt(Get2DACache(sSpkn, "SpellLevel1", 0)))
                        return STAGE_SPELLS_1;
                }       
            }
            case STAGE_SPELLS_1: {
                if (nClass == CLASS_TYPE_WIZARD || nClass == CLASS_TYPE_SORCERER || nClass == CLASS_TYPE_BARD)
                {
                    return STAGE_SPELLS_CHECK; // checks both 0 and 1 level spells
                }
            }
            case STAGE_SPELLS_CHECK: {
                if (nClass == CLASS_TYPE_WIZARD || nClass == CLASS_TYPE_SORCERER || nClass == CLASS_TYPE_DRUID)
                {
                    return STAGE_FAMILIAR; // also does animal companion
                }
            }
            case STAGE_FAMILIAR_CHECK: {
                if (nClass == CLASS_TYPE_CLERIC)
                {
                    return STAGE_DOMAIN;
                }
            }
            default:
                return STAGE_APPEARANCE;
        }
    }
    else
    {
        int nAppearance = GetLocalInt(OBJECT_SELF, "Appearance");
        // the model type determines if the model is dynamic, can have wings or tails
        string sModelType = Get2DACache("appearance", "MODELTYPE", nAppearance);
        switch(nStage) // with no breaks to go all the way through
        {
            case STAGE_SOUNDSET:
            case STAGE_SOUNDSET_CHECK:
            {
                if (sModelType == "P")
                    return STAGE_HEAD;
            }
            case STAGE_HEAD:
            case STAGE_HEAD_CHECK:
            {
                if (sModelType == "P")
                    return STAGE_TATTOO;
            }
            case STAGE_TATTOO:
            case STAGE_TATTOO_CHECK:
            {
                if (sModelType == "P" || TestStringAgainstPattern("**W**", sModelType))
                    return STAGE_WINGS;
            }
            case STAGE_WINGS_CHECK:
            {
                if (sModelType == "P" || TestStringAgainstPattern("**T**", sModelType))
                    return STAGE_TAIL;
            }
            case STAGE_TAIL_CHECK:
            {
                if (sModelType == "P")
                    return STAGE_SKIN_COLOUR;
            }
            default:
                return FINAL_STAGE;
        }
    }
    return -1; // silly compiler
}

void AddCachedChoice(string sText, int nValue, object oPC = OBJECT_SELF)
{
    // pretty much the same as the AddChoice() function
    if(!array_exists(oPC, "CachedChoiceTokens"))
        array_create(oPC, "CachedChoiceTokens");
    if(!array_exists(oPC, "CachedChoiceValues"))
        array_create(oPC, "CachedChoiceValues");
    array_set_string(oPC, "CachedChoiceTokens", array_get_size(oPC, "CachedChoiceTokens"), sText);
    array_set_int   (oPC, "CachedChoiceValues", array_get_size(oPC, "CachedChoiceValues"), nValue);
}

void ClearCachedChoices(object oPC = OBJECT_SELF)
{
    array_delete(oPC, "CachedChoiceTokens");
    array_delete(oPC, "CachedChoiceValues");
}

void AddChoicesFromCache(object oPC = OBJECT_SELF)
{
    int nArraySize = array_get_size(oPC, "CachedChoiceTokens");
    int i, nValue;
    string sText;
    for(i = 0; i < nArraySize; i++)
    {
        sText = array_get_string(oPC, "CachedChoiceTokens", i);
        nValue = array_get_int(oPC, "CachedChoiceValues", i);
        AddChoice(sText, nValue);
    }
}

int GetMeetsANDPreReq(string sPreReqFeat1, string sPreReqFeat2)
{
    // are there any prereq?
    if (sPreReqFeat1 == "****" && sPreReqFeat2 == "****")
        return TRUE;
    // test if the PC meets the first prereq
    // if not, exit
    if(!PreReqFeatArrayLoop(StringToInt(sPreReqFeat1)))
        return FALSE;
    // got this far, then the first prereq was met
    // is there a second prereq? If not, done
    if (sPreReqFeat2 == "****")
        return TRUE;
    // test if the PC meets the second one
    if(!PreReqFeatArrayLoop(StringToInt(sPreReqFeat2)))
        return FALSE;
    // got this far, so second one matched too
    return TRUE;
}

int GetMeetsORPreReq(string sOrReqFeat0, string sOrReqFeat1, string sOrReqFeat2, string sOrReqFeat3, string sOrReqFeat4)
{
    // are there any prereq
    if (sOrReqFeat0 == "****")
        return TRUE;
    // first one
    if(PreReqFeatArrayLoop(StringToInt(sOrReqFeat0)))
        return TRUE;
    // second one
    if(PreReqFeatArrayLoop(StringToInt(sOrReqFeat1)))
        return TRUE;
    // third one
    if(PreReqFeatArrayLoop(StringToInt(sOrReqFeat2)))
        return TRUE;
    // 4th one
    if(PreReqFeatArrayLoop(StringToInt(sOrReqFeat3)))
        return TRUE;
    // 5th one
    if(PreReqFeatArrayLoop(StringToInt(sOrReqFeat4)))
        return TRUE;
    // no match
    return FALSE;
}

int PreReqFeatArrayLoop(int nOrReqFeat)
{
    // as alertness is stored in the array as -1
    if (nOrReqFeat == 0)
        nOrReqFeat = -1;
    int i = 0;
    while (i != array_get_size(OBJECT_SELF, "Feats"))
    {
        int nFeat  = array_get_int(OBJECT_SELF, "Feats", i);
        if(nFeat == nOrReqFeat) // if there's a match, the prereq are met
            return TRUE;
        i++;
    }
    // otherwise no match
    return FALSE;
}

int GetMeetSkillPrereq(string sReqSkill, string sReqSkill2, string sReqSkillRanks,  string sReqSkillRanks2)
{
    if(sReqSkill == "****" && sReqSkill2 == "****")
        return TRUE;
    // test if the PC meets the first prereq
    if(!CheckSkillPrereq(sReqSkill, sReqSkillRanks))
        return FALSE;
    
    // got this far, then the first prereq was met
	// is there a second prereq? If not, done
    if(sReqSkill2 == "****")
        return TRUE;
    if(!CheckSkillPrereq(sReqSkill2, sReqSkillRanks2))
        return FALSE;
    // got this far, so second one matched too
    return TRUE;
}

int CheckSkillPrereq(string sReqSkill, string sReqSkillRanks)
{
    // for skill focus feats
    if (sReqSkillRanks == "0" || sReqSkillRanks == "****") // then it just requires being able to put points in the skill
    {
        // if requires animal empathy, but the PC can't take ranks in it
        if(sReqSkill == "0" && !GetLocalInt(OBJECT_SELF, "bHasAnimalEmpathy"))
            return FALSE;
        // if requires UMD, but the PC can't take ranks in it
        if(sReqSkill == IntToString(SKILL_USE_MAGIC_DEVICE) && !GetLocalInt(OBJECT_SELF, "bHasUMD"))
            return FALSE;
    }
    else // test if the PC has enough ranks in the skill
    {
        int nSkillPoints = array_get_int(OBJECT_SELF, "Skills", StringToInt(sReqSkill));
        if (nSkillPoints < StringToInt(sReqSkillRanks))
            return FALSE;
    }
    // get this far then not failed any of the prereq
    return TRUE;
}

void SetWizCantrips(int iSpellschool)
{
    string sOpposition = "";
    // if not a generalist
    if(iSpellschool)
    {
        sOpposition = Get2DACache("spellschools", "Letter", StringToInt(Get2DACache("spellschools", "Opposition", iSpellschool)));
    }
    
    array_create(OBJECT_SELF, "SpellLvl0");
    string sSQL = "SELECT rowid FROM prc_cached2da_spells WHERE (wiz_sorc = '0') AND (school != '"+sOpposition+"')";
    PRC_SQLExecDirect(sSQL);
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        int nRow = StringToInt(PRC_SQLGetData(1));
        array_set_int(OBJECT_SELF, "SpellLvl0", array_get_size(OBJECT_SELF, "SpellLvl0"),nRow);
    }
}

int GetIsSpellKnown(int nSpell, int nSpellLevel)
{
    // spell 0 is a level 6 spell, so no need to do the 0 == -1 workaround
    int i = 0;
    string sArray = "SpellLvl" + IntToString(nSpellLevel);
    // if the array doesn't exist then there won't be a match
    if (!array_exists(OBJECT_SELF, sArray))
        return FALSE;
    while (i != array_get_size(OBJECT_SELF, sArray))
    {
        int nKnownSpell  = array_get_int(OBJECT_SELF, sArray, i);
        if(nKnownSpell == nSpell) // if there's a match, don't add it
            return TRUE;
        i++;
    }
    // otherwise no match
    return FALSE;
}

void SetupRacialAppearances()
{
    int nRace = GetLocalInt(OBJECT_SELF, "Race");
    int nSex  = GetLocalInt(OBJECT_SELF, "Gender");
    string sSex = nSex == 0 ? "male" : "female";
    object oPC = OBJECT_SELF;

    // see if the PC's race and gender combination has an entry in racialappearances.2da
    /*
        SELECT appearance.data FROM prc_cached2da, prc_cached2da AS appearance, prc_cached2da AS gender
        WHERE prc_cached2da.rowid = appearance.rowid 
        AND prc_cached2da.rowid = gender.rowid
        AND prc_cached2da.file = "racialappearance" AND appearance.file = "racialappearance" AND gender.file = "racialappearance"
        AND prc_cached2da.columnid = "Race" AND appearance.columnid = "Appearance" AND gender.columnid = "Gender"
        AND prc_cached2da.data = "<race>" AND (gender.data = "<gender>" OR gender.data = "2");
     */

    /*
    string sSQL = "SELECT appearance.data FROM prc_cached2da, prc_cached2da AS appearance, prc_cached2da AS gender " +
    "WHERE prc_cached2da.rowid = appearance.rowid AND prc_cached2da.rowid = gender.rowid " +
    "AND prc_cached2da.file = 'racialappearance' AND appearance.file = 'racialappearance' AND gender.file = 'racialappearance' " +
    "AND prc_cached2da.columnid = 'race' AND appearance.columnid = 'appearance' AND gender.columnid = 'gender' " +
    "AND prc_cached2da.data = " + IntToString(nRace) + " AND (gender.data = " + IntToString(nSex) + " OR gender.data = '2')";
    */
    string sSQL = "SELECT data FROM prc_cached2da WHERE file = 'racialappear' AND rowid = "+IntToString(nRace)+
                  " AND (columnid = '"+sSex+"1' OR columnid = '"+sSex+"2' OR columnid = '"+sSex+"3' OR columnid = '"+sSex+"4' OR columnid = '"+sSex+"5')";

    PRC_SQLExecDirect(sSQL);
    int i;
    array_create(oPC, "AppearanceChoices");
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
         string sTemp = PRC_SQLGetData(1);
         if(sTemp != "****")
         {
             i = StringToInt(sTemp); // appearance
             // as doing a Get2DACache() call here will overwrite the sql result, add to an array to do this later
             array_set_int (oPC, "AppearanceChoices", array_get_size(oPC, "AppearanceChoices"), i);
         }
    }
    // if there's any values in the array use those for choices
    int nNumberOfChoices = array_get_size(oPC, "AppearanceChoices");
    if (nNumberOfChoices)
    {
        // loop through the array and add all the choices
        i = 0;
        while (i < nNumberOfChoices)
        {
            AddAppearanceChoice(array_get_int(oPC, "AppearanceChoices", i));
            i++;
        }
    }
    else // it is the appearance given at the race stage
    {
        // the only 'choice'
        AddAppearanceChoice(StringToInt(Get2DACache("racialtypes", "Appearance", nRace)), TRUE);
    }
    // tidy up
    array_delete(oPC, "AppearanceChoices");
    DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
}

void AddAppearanceChoice(int nType, int nOnlyChoice = FALSE)
{
    // get the appearance type name
    string sName;
    int nStrRef = StringToInt(Get2DACache("appearance", "STRING_REF", nType));
    if(nStrRef)
        sName = GetStringByStrRef(nStrRef);
    else
        sName = Get2DACache("appearance", "LABEL", nType);
    // if there's only one option, it has already been stored on the PC
    // at the race choice stage
    if (nOnlyChoice) 
    {
        // add a "continue" so player doesn't expect a choice
        sName = GetStringByStrRef(39) + " (" + sName + ")";
        // marker to skip the check stage
        nType = -1;
    }
    AddChoice(sName, nType);
}

void SetupHeadChoices()
{
    int nGender = GetLocalInt(OBJECT_SELF, "Gender");
    int nAppearance = GetLocalInt(OBJECT_SELF, "Appearance");
    // determine the number of heads (based on both appearance and gender)
    int nHeadNumber;
    int nHead2Start; // some appearances have a second run of head numbers
    int nHead2End;
    // change numbers to account for custom heads here
    if(GetPRCSwitch(MARKER_CEP2)) // add in the extra (consecutive) heads
    {
        if(nAppearance == APPEARANCE_TYPE_HUMAN
        || nAppearance == APPEARANCE_TYPE_HALF_ELF)
        {
            if(nGender == GENDER_MALE)
            {
                nHeadNumber = 49;
                nHead2Start = 100;
                nHead2End = 113;
            }
            else if (nGender == GENDER_FEMALE)
            {
                nHeadNumber = 49;
                nHead2Start = 100;
                nHead2End = 114;
            }
        }
        else if (nAppearance == APPEARANCE_TYPE_ELF)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 34;
            else if (nGender == GENDER_FEMALE)
            {
                nHeadNumber = 43;
                nHead2Start = 179;
                nHead2End = 180;
            }
        }
        else if (nAppearance == APPEARANCE_TYPE_HALFLING)
        {
            if(nGender == GENDER_MALE)
            {
                nHeadNumber = 26;
                nHead2Start = 160;
                nHead2End = 161;
            }
            else if (nGender == GENDER_FEMALE)
            {
                nHeadNumber = 15;
                nHead2Start = 161;
                nHead2End = 167;
            }
        }
        else if (nAppearance == APPEARANCE_TYPE_HALF_ORC)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 31;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 15;
        }
        else if (nAppearance == APPEARANCE_TYPE_DWARF)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 24;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 23;
        }
        else if(nAppearance == APPEARANCE_TYPE_GNOME)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 35;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 10;
        }
        else if (nAppearance == APPEARANCE_TYPE_CEP_BROWNIE)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 12;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 11;
        }
        else if (nAppearance == APPEARANCE_TYPE_CEP_WEMIC)
        {
            if (nGender == GENDER_MALE)
                nHeadNumber = 4;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 2;
        }
    }
    else
    {
        if(nAppearance == APPEARANCE_TYPE_HUMAN
            || nAppearance == APPEARANCE_TYPE_HALF_ELF)
        {
            if(nGender == GENDER_MALE)
            {
                nHeadNumber = 34;
                nHead2Start = 140;
                nHead2End = 143;
            }
            else if (nGender == GENDER_FEMALE)
            {
                nHeadNumber = 27;
                nHead2Start = 140;
                nHead2End = 143;
            }
        }
        else if (nAppearance == APPEARANCE_TYPE_ELF)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 18;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 16;
        }
        else if (nAppearance == APPEARANCE_TYPE_HALFLING)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 10;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 11;
        }
        else if (nAppearance == APPEARANCE_TYPE_HALF_ORC)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 13;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 12;
        }
        else if (nAppearance == APPEARANCE_TYPE_DWARF)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 13;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 12;
        }
        else if(nAppearance == APPEARANCE_TYPE_GNOME)
        {
            if(nGender == GENDER_MALE)
                nHeadNumber = 13;
            else if (nGender == GENDER_FEMALE)
                nHeadNumber = 9;
        }
    }
    int i;
    for(i=1;i<= nHeadNumber;i++)
        AddChoice(IntToString(i), i);
    if (nHead2Start)
    {
        for(i = nHead2Start;i <= nHead2End;i++)
        AddChoice(IntToString(i), i);
    }
    
    // and the non consecutive heads for the CEP
    if((nAppearance == APPEARANCE_TYPE_HUMAN
        || nAppearance == APPEARANCE_TYPE_HALF_ELF) && GetPRCSwitch(MARKER_CEP2))
    {
        if(nGender == GENDER_MALE)
        {
            AddChoice(IntToString(140), 140);
            AddChoice(IntToString(141), 141);
            AddChoice(IntToString(142), 142);
            AddChoice(IntToString(143), 143);
            AddChoice(IntToString(155), 155);
        }
        else if (nGender == GENDER_FEMALE)
        {
            AddChoice(IntToString(140), 140);
            AddChoice(IntToString(141), 141);
            AddChoice(IntToString(142), 142);
            AddChoice(IntToString(143), 143);
            AddChoice(IntToString(147), 147);
            AddChoice(IntToString(148), 148);
            AddChoice(IntToString(149), 149);
            AddChoice(IntToString(150), 150);
            AddChoice(IntToString(151), 151);
            AddChoice(IntToString(152), 152);
            AddChoice(IntToString(155), 155);
            AddChoice(IntToString(180), 180);
            AddChoice(IntToString(181), 181);
        }
    }
}

int MapAppearanceToRace(int nAppearance)
{
    switch(nAppearance)
    {
        case APPEARANCE_TYPE_DWARF:
            return RACIAL_TYPE_DWARF;
            break;
        case APPEARANCE_TYPE_ELF:
            return RACIAL_TYPE_ELF;
            break;
        case APPEARANCE_TYPE_GNOME:
            return RACIAL_TYPE_GNOME;
            break;
        case APPEARANCE_TYPE_HALFLING:
            return RACIAL_TYPE_HALFLING;
            break;
        case APPEARANCE_TYPE_HALF_ORC:
            return RACIAL_TYPE_HALFORC;
            break;
        case APPEARANCE_TYPE_HUMAN:
        case APPEARANCE_TYPE_HALF_ELF: // half-elves get human portraits
            return RACIAL_TYPE_HUMAN;
            break;
        default: 
            return -1;
    }
    return -1; // silly compiler
}

void Do2daLoop(string s2da, string sColumnName, int nFileEnd)
{
    /* SELECT statement
     * SELECT rowid, data FROM prc_cached2da
     * WHERE file = <s2DA> AND columnid = <sColumnName> AND rowid >= <nFileEnd>
     */
     string sSQL = "SELECT rowid, data FROM prc_cached2da WHERE file = '"+ s2da +"' AND columnid = '" + sColumnName +"' AND  rowid <= " + IntToString(nFileEnd);
     PRC_SQLExecDirect(sSQL);
     int i;
     string sData;
     while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
     {
         i = StringToInt(PRC_SQLGetData(1)); // rowid
         sData = GetStringByStrRef(StringToInt(PRC_SQLGetData(2))); // data
         AddChoice(sData, i);
         
     }
}

void DoRacialtypesLoop()
{
    // get the results 25 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    /* SELECT statement
    SELECT rowid, Name FROM prc_cached2da_racialtypes
    WHERE PlayerRace = 1
    LIMIT 25 OFFSET <nReali>
    */
    string sSQL = "SELECT rowid, name FROM prc_cached2da_racialtypes WHERE (playerrace = 1) LIMIT 25 OFFSET "+IntToString(nReali);
    PRC_SQLExecDirect(sSQL);
    // to keep track of where in the 25 rows we stop getting a result
    int nCounter = 0;
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        int nRace = StringToInt(PRC_SQLGetData(1)); // rowid
        int bIsTakeable = TRUE;
        
        // check for right drow gender IF the switch is set
        if(GetPRCSwitch(PRC_CONVOCC_DROW_ENFORCE_GENDER))
        {
            if(nRace == RACIAL_TYPE_DROW_FEMALE
                && GetLocalInt(OBJECT_SELF, "Gender") == GENDER_MALE)
                bIsTakeable = FALSE;
            if(nRace == RACIAL_TYPE_DROW_MALE
                && GetLocalInt(OBJECT_SELF, "Gender") == GENDER_FEMALE)
                bIsTakeable = FALSE;
        }
        
        // add the choices, choice number is race rowid/constant value
        if(bIsTakeable)
        {
            string sName = GetStringByStrRef(StringToInt(PRC_SQLGetData(2)));
            AddChoice(sName, nRace);
        }
    }
    
    // IF there were 25 rows, carry on
    if(nCounter == 25)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+25);
        DelayCommand(0.01, DoRacialtypesLoop());
    }
    else // there were less than 25 rows, it's the end of the 2da
    {
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
        DeleteLocalInt(OBJECT_SELF, "i");
        return;
    }
}

void DoClassesLoop()
{
    // remove if decide not to make the convo wait
    if(GetLocalInt(OBJECT_SELF, "DynConv_Waiting") == FALSE)
        return;
    // get the results 25 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    /*
    SELECT `rowid`, `PreReqTable` FROM `prc_cached2da_classes`
    WHERE (`PlayerClass` = 1) AND (`XPPenalty` = 1) 
    LIMIT 25 OFFSET <nReali>
    */
    string sSQL = "SELECT rowid, prereqtable FROM prc_cached2da_classes WHERE (playerclass = 1) AND (xppenalty = 1) LIMIT 25 OFFSET "+IntToString(nReali);
    PRC_SQLExecDirect(sSQL);
    // to keep track of where in the 25 rows we stop getting a result
    int nCounter = 0;
    int nClass;
    string sPreReq, sReqType, sParam1, sParam2;
    // this needs storing in a temp array because the 2da cache retrieval will clear
    // the query above if both steps are done in the same loop
    // two parallel arrays as there's no struct arrays
    array_create(OBJECT_SELF, "temp_classes");
    array_create(OBJECT_SELF, "temp_class_prereq");
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        nClass = StringToInt(PRC_SQLGetData(1)); // rowid
        array_set_int(OBJECT_SELF, "temp_classes", array_get_size(OBJECT_SELF, "temp_classes"), nClass);
        sPreReq = PRC_SQLGetData(2); // PreReq tablename
        array_set_string(OBJECT_SELF, "temp_class_prereq", array_get_size(OBJECT_SELF, "temp_class_prereq"), sPreReq);

    }
    
    // loop through the temp array to check for banned classes
    int i;
    for (i=0; i < array_get_size(OBJECT_SELF, "temp_classes"); i++)
    {
        nClass = array_get_int(OBJECT_SELF, "temp_classes", i); // class
        sPreReq = array_get_string(OBJECT_SELF, "temp_class_prereq", i); // prereq table name
        int j = 0;
        // check if this base class is allowed
        do
        {
            sReqType = Get2DACache(sPreReq, "ReqType", j);
            if (sReqType == "VAR") // if we've found the class allowed variable
            {
                sParam1 = Get2DACache(sPreReq, "ReqParam1", j);
                if(!GetLocalInt(OBJECT_SELF, sParam1)) // if the class is allowed
                {
                    // adds the class to the choice list
                    string sName = GetStringByStrRef(StringToInt(Get2DACache("classes", "Name", nClass)));
                    AddChoice(sName, nClass);
                }
            } // end of if (sReqType == "VAR")
            j++;
            
        } while (sReqType != "VAR"); // terminates as soon as we get the allowed variable
        
    } // end of for loop
    // clean up
    array_delete(OBJECT_SELF, "temp_classes");
    array_delete(OBJECT_SELF, "temp_class_prereq");
    
    // IF there were 25 rows, carry on
    if(nCounter == 25)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+25);
        DelayCommand(0.01, DoClassesLoop());
    }
    else // there were less than 25 rows, it's the end of the 2da
    {
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
        DeleteLocalInt(OBJECT_SELF, "i");
        return;
    }
}

void DoSkillsLoop()
{
    if(GetPRCSwitch(PRC_CONVOCC_ALLOW_SKILL_POINT_ROLLOVER))
    {
        // add the "store" option
        AddChoice("Store all remaining points.", -2);
    }
    // get the class of the PC
    int nClass = GetLocalInt(OBJECT_SELF, "Class");
    // get the cls_skill_*** 2da to use
    string sFile = GetStringLowerCase(Get2DACache("classes", "SkillsTable", nClass));
    string sSQL = "SELECT rowid, data FROM prc_cached2da WHERE file = '" + sFile + "' AND columnid= 'skillindex'";
    PRC_SQLExecDirect(sSQL);

    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        int nRow = StringToInt(PRC_SQLGetData(1));
        int nSkillID = StringToInt(PRC_SQLGetData(2)); // line of skills.2da for the skill
        int nPoints = GetLocalInt(OBJECT_SELF, "Points");
        // get the skill name
        string sName = GetStringByStrRef(StringToInt(Get2DACache("skills", "Name", nSkillID)));
        // class skill or truespeak and PC is a truenamer
        if(Get2DAString(sFile, "ClassSkill", nRow) == "1" || (nSkillID == SKILL_TRUESPEAK && nClass == CLASS_TYPE_TRUENAMER))
        {
            sName += " " + GetStringByStrRef(52951); // (Class Skill)
            // check there's not already 4 points in there
            int nStoredPoints = array_get_int(OBJECT_SELF, "Skills", nSkillID);
            if (nStoredPoints < 4) // level (ie 1) + 3
            {
                sName += " : "+IntToString(nStoredPoints);
                // only add if there's less than the maximum points allowed
                AddChoice(sName, nRow); // uses cls_skill_*** rowid as choice number
            }
        }
        else // cross-class skill
        {
            // check there's not already 2 points in there
            int nStoredPoints = array_get_int(OBJECT_SELF, "Skills", nSkillID);
            // if there's only 1 point left, then no cross class skills
            if (nStoredPoints < 2 && nPoints > 1) // level (ie 1) + 1
            {
                sName += " : "+IntToString(nStoredPoints);
                // only add if there's less than the maximum points allowed
                AddChoice(sName, nRow); // uses cls_skill_*** rowid as choice number
            }
        }
    }
    // if the dynamic convo is set waiting (first time through only)
    // then mark as done
    if (GetLocalInt(OBJECT_SELF, "DynConv_Waiting"))
    {
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
    }
}

void DoFeatLoop(int nClassFeatStage = FALSE)
{
    /* TODO - scripting feat enforcement */
    string sSQL;
    object oPC = OBJECT_SELF;

    // get the information needed to work out if the prereqs are met
    int nSex = GetLocalInt(oPC, "Gender");
    int nRace = GetLocalInt(oPC, "Race");
    int nClass = GetLocalInt(oPC, "Class");
    int nStr = GetLocalInt(oPC, "Str");
    int nDex = GetLocalInt(oPC, "Dex");
    int nCon = GetLocalInt(oPC, "Con");
    int nInt = GetLocalInt(oPC, "Int");
    int nWis = GetLocalInt(oPC, "Wis");
    int nCha = GetLocalInt(oPC, "Cha"); 
    int nOrder = GetLocalInt(oPC, "LawfulChaotic");
    int nMoral = GetLocalInt(oPC, "GoodEvil");

    //add racial ability alterations
    nStr += StringToInt(Get2DACache("racialtypes", "StrAdjust", nRace));
    nDex += StringToInt(Get2DACache("racialtypes", "DexAdjust", nRace));
    nCon += StringToInt(Get2DACache("racialtypes", "ConAdjust", nRace));
    nInt += StringToInt(Get2DACache("racialtypes", "IntAdjust", nRace));
    nWis += StringToInt(Get2DACache("racialtypes", "WisAdjust", nRace));
    nCha += StringToInt(Get2DACache("racialtypes", "ChaAdjust", nRace));

    // get BAB
    int nBAB = StringToInt(Get2DACache(Get2DACache("classes", "AttackBonusTable", nClass), "BAB", 0));

    // get fortitude save
    int nFortSave = StringToInt(Get2DACache(Get2DACache("classes","SavingThrowTable" , nClass), "FortSave", 0));

    // get the results 5 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");

    if (!nClassFeatStage) // select the general feats
    {
        /*
        SELECT `rowid`, `feat`, `PREREQFEAT1`, `PREREQFEAT2`, `OrReqFeat0`, `OrReqFeat1`, `OrReqFeat2`, `OrReqFeat3`, `OrReqFeat4`,
        `REQSKILL`, `REQSKILL2`, `ReqSkillMinRanks`, `ReqSkillMinRanks2`
        FROM `prc_cached2da_feat`
        WHERE (`feat` != '****') AND (`PreReqEpic` != 1)
        AND (`MinLevel` = '****' OR `MinLevel` = '1')
        AND `ALLCLASSESCANUSE` = 1
        AND `minattackbonus` <= <nBAB>
        AND `minspelllvl` <= 1
        AND `minstr`<= <nStr>
        AND `mindex`<= <nDex>
        AND `mincon`<= <nCon>
        AND `minint`<= <nInt>
        AND `minwis`<= <nWis>
        AND `mincha`<= <nCha>
        AND `MinFortSave` <= <nFortSave>
        */

        sSQL = "SELECT rowid, feat, prereqfeat1, prereqfeat2, orreqfeat0, orreqfeat1, orreqfeat2, orreqfeat3, orreqfeat4, "
                +"reqskill, reqskill2, reqskillminranks, reqskillminranks2 FROM prc_cached2da_feat"
                +" WHERE (feat != '****') AND (prereqepic != 1)"
                +" AND (minlevel = '****' OR minlevel = '1')"
                +" AND (allclassescanuse = 1)"
                +" AND (minattackbonus <= "+IntToString(nBAB)+")"
                +" AND (minspelllvl <= 1)"
                +" AND (minstr <= "+IntToString(nStr)+")"
                +" AND (mindex <= "+IntToString(nDex)+")"
                +" AND (mincon <= "+IntToString(nCon)+")"
                +" AND (minint <= "+IntToString(nInt)+")"
                +" AND (minwis <= "+IntToString(nWis)+")"
                +" AND (mincha <= "+IntToString(nCha)+")"
                +" AND (minfortsave <= "+IntToString(nFortSave)+")"
                +" LIMIT 5 OFFSET "+IntToString(nReali);
    }
    else // select the class feats
    {
        // get which cls_feat_*** 2da to use
    string sFile = GetStringLowerCase(Get2DACache("classes", "FeatsTable", nClass));

        /*
        SELECT prc_cached2da_cls_feat.FeatIndex, prc_cached2da_cls_feat.FEAT, 
            prc_cached2da_feat.PREREQFEAT1, prc_cached2da_feat.PREREQFEAT2, 
            prc_cached2da_feat.OrReqFeat0, prc_cached2da_feat.OrReqFeat1, prc_cached2da_feat.OrReqFeat2, prc_cached2da_feat.OrReqFeat3, prc_cached2da_feat.OrReqFeat4, 
            prc_cached2da_feat.REQSKILL, prc_cached2da_feat.REQSKILL2,
            prc_cached2da_feat.ReqMinSkillRanks, prc_cached2da_feat.ReqMinSkillRanks2
        FROM prc_cached2da_cls_feat INNER JOIN prc_cached2da_feat
        WHERE (prc_cached2da_feat.FEAT != '****') AND (prc_cached2da_cls_feat.FeatIndex != '****')
            AND (prc_cached2da_cls_feat.file = '<cls_feat***>')
            AND (prc_cached2da_cls_feat.List <= 1)
            AND (prc_cached2da_cls_feat.GrantedOnLevel <= 1)
            AND (prc_cached2da_feat.rowid = prc_cached2da_cls_feat.FeatIndex)
            AND (`PreReqEpic` != 1)
            AND (`MinLevel` = '****' OR `MinLevel` = '1')
            AND `ALLCLASSESCANUSE` = 0
            AND `minattackbonus` <= <nBAB>
            AND `minspelllvl` <= 1
            AND `minstr`<= <nStr>
            AND `mindex`<= <nDex>
            AND `mincon`<= <nCon>
            AND `minint`<= <nInt>
            AND `minwis`<= <nWis>
            AND `mincha`<= <nCha>
            AND `MinFortSave` <= <nFortSave>
        */

        sSQL = "SELECT prc_cached2da_feat.rowid, feat, prereqfeat1, prereqfeat2, "
                +"orreqfeat0, orreqfeat1, orreqfeat2, orreqfeat3, orreqfeat4, "
                +"reqskill, reqskill2, reqskillminranks, reqskillminranks2"
                +" FROM prc_cached2da_feat INNER JOIN prc_cached2da_cls_feat"
                +" WHERE (prc_cached2da_feat.feat != '****') AND (prc_cached2da_cls_feat.featindex != '****')"
                +" AND (prc_cached2da_cls_feat.file = '" + sFile + "')"
                +" AND (prc_cached2da_cls_feat.list <= 1)"
                +" AND (prc_cached2da_cls_feat.grantedonlevel <= 1)"
                +" AND (prc_cached2da_feat.rowid = prc_cached2da_cls_feat.featindex)"
                +" AND (prereqepic != 1)"
                +" AND (minlevel = '****' OR minlevel = '1')"
                +" AND (allclassescanuse != 1)"
                +" AND (minattackbonus <= "+IntToString(nBAB)+")"
                +" AND (minspelllvl <= 1)"
                +" AND (minstr <= "+IntToString(nStr)+")"
                +" AND (mindex <= "+IntToString(nDex)+")"
                +" AND (mincon <= "+IntToString(nCon)+")"
                +" AND (minint <= "+IntToString(nInt)+")"
                +" AND (minwis <= "+IntToString(nWis)+")"
                +" AND (mincha <= "+IntToString(nCha)+")"
                +" AND (minfortsave <= "+IntToString(nFortSave)+")"
                +" LIMIT 5 OFFSET "+IntToString(nReali);
    }

    // debug print the sql statement
    if(DEBUG)
    {
        DoDebug(sSQL);
    }

    PRC_SQLExecDirect(sSQL);
    // to keep track of where in the 25 rows we stop getting a result
    int nCounter = 0;
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        int nRow = StringToInt(PRC_SQLGetData(1));
        if((nRow > 5320  && nRow < 7700)
        || (nRow > 7938  && nRow < 23519)
        || (nRow > 23601 && nRow < 24000))
            continue;

        int nStrRef = StringToInt(PRC_SQLGetData(2));
        string sName = GetStringByStrRef(nStrRef);
        string sPreReqFeat1 = PRC_SQLGetData(3);
        string sPreReqFeat2 = PRC_SQLGetData(4);
        string sOrReqFeat0 = PRC_SQLGetData(5);
        string sOrReqFeat1 = PRC_SQLGetData(6);
        string sOrReqFeat2 = PRC_SQLGetData(7);
        string sOrReqFeat3 = PRC_SQLGetData(8);
        string sOrReqFeat4 = PRC_SQLGetData(9);
        string sReqSkill = PRC_SQLGetData(10);
        string sReqSkill2 = PRC_SQLGetData(11);
        string sReqSkillRanks = PRC_SQLGetData(12);
        string sReqSkillRanks2 = PRC_SQLGetData(13);

        // check AND feat prerequisites
        if (GetMeetsANDPreReq(sPreReqFeat1, sPreReqFeat2))
        {
            // check OR feat prerequisites
            if (GetMeetsORPreReq(sOrReqFeat0, sOrReqFeat1, sOrReqFeat2, sOrReqFeat3, sOrReqFeat4))
            {
                // check skill prerequisites
                if(GetMeetSkillPrereq(sReqSkill, sReqSkill2, sReqSkillRanks, sReqSkillRanks2))
                {
                    // check they don't have it already
                    if(!PreReqFeatArrayLoop(nRow))
                    {
                        AddCachedChoice(sName, nRow);
                    }
                    else
                    {
                        if(DEBUG) DoDebug("Already picked feat " + IntToString(nRow) + ". Not added!");
                    }
                }
                else
                {
                    if(DEBUG) DoDebug("Not met skill prereq for feat " + IntToString(nRow) + ". Not added!");
                }
            }
            else
            {
                if(DEBUG) DoDebug("Not met OR prereqfeat test for feat " + IntToString(nRow) + ". Not added!");
            }
        }
        else
        {
            if(DEBUG) DoDebug("Not met AND prereqfeat test for feat " + IntToString(nRow) + ". Not added!");
        }
    } // end of while(PRC_SQLFetch() == PRC_SQL_SUCCESS)

    if(nCounter == 5)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+5);
        DelayCommand(0.01, DoFeatLoop(nClassFeatStage));
    }
    else // there were less than 5 rows, it's the end of the 2da
    {
        if(nClassFeatStage)
        {
            AddChoicesFromCache();
            FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
            if(DEBUG) DoDebug("Finished class feats");
            DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
            DeleteLocalInt(OBJECT_SELF, "i");
            return;
        }
        else // run again to select class feats
        {
            nClassFeatStage = TRUE;
            if(DEBUG) DoDebug("Finished general feats");
            DeleteLocalInt(OBJECT_SELF, "i");
            DelayCommand(0.01, DoFeatLoop(nClassFeatStage));
        }
    }
}

void DoBonusFeatLoop()
{
    /* TODO - scripting feat enforcement */
    string sSQL;
    object oPC = OBJECT_SELF;

    int nFeatsRemaining = GetLocalInt(OBJECT_SELF, "Points");
    int nClass = GetLocalInt(oPC, "Class");

    if (nClass == CLASS_TYPE_PSION && nFeatsRemaining == 2)
    {
        // then skip everything and just list the disciplines
        /*
        SELECT rowid, FEAT FROM prc_cached2da_feat
        WHERE rowid >= 3554 AND rowid <= 3559
        */
        sSQL = "SELECT rowid, feat FROM prc_cached2da_feat WHERE rowid >= 3554 AND rowid <= 3559";
        PRC_SQLExecDirect(sSQL);
        // to keep track of where in the 25 rows we stop getting a result
        int nCounter = 0;
        while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
        {
            nCounter++;
            int nRow = StringToInt(PRC_SQLGetData(1));
            int nStrRef = StringToInt(PRC_SQLGetData(2));
            string sName = GetStringByStrRef(nStrRef);
            AddChoice(sName, nRow);
        }

        AddChoicesFromCache();
        if(DEBUG) DoDebug("Finished bonus feats");
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");

        return;
    }

    // get the information needed to work out if the prereqs are met
    int nSex = GetLocalInt(oPC, "Gender");
    int nRace = GetLocalInt(oPC, "Race");

    int nStr = GetLocalInt(oPC, "Str");
    int nDex = GetLocalInt(oPC, "Dex");
    int nCon = GetLocalInt(oPC, "Con");
    int nInt = GetLocalInt(oPC, "Int");
    int nWis = GetLocalInt(oPC, "Wis");
    int nCha = GetLocalInt(oPC, "Cha"); 
    int nOrder = GetLocalInt(oPC, "LawfulChaotic");
    int nMoral = GetLocalInt(oPC, "GoodEvil");

    //add racial ability alterations
    nStr += StringToInt(Get2DACache("racialtypes", "StrAdjust", nRace));
    nDex += StringToInt(Get2DACache("racialtypes", "DexAdjust", nRace));
    nCon += StringToInt(Get2DACache("racialtypes", "ConAdjust", nRace));
    nInt += StringToInt(Get2DACache("racialtypes", "IntAdjust", nRace));
    nWis += StringToInt(Get2DACache("racialtypes", "WisAdjust", nRace));
    nCha += StringToInt(Get2DACache("racialtypes", "ChaAdjust", nRace));

    // get BAB
    int nBAB = StringToInt(Get2DACache(Get2DACache("classes", "AttackBonusTable", nClass), "BAB", 0));

    // get fortitude save
    int nFortSave = StringToInt(Get2DACache(Get2DACache("classes","SavingThrowTable" , nClass), "FortSave", 0));

    // get the results 5 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");

    // get which cls_feat_*** 2da to use
    string sFile = GetStringLowerCase(Get2DACache("classes", "FeatsTable", nClass));

        /*
        SELECT prc_cached2da_cls_feat.FeatIndex, prc_cached2da_cls_feat.FEAT, 
            prc_cached2da_feat.PREREQFEAT1, prc_cached2da_feat.PREREQFEAT2, 
            prc_cached2da_feat.OrReqFeat0, prc_cached2da_feat.OrReqFeat1, prc_cached2da_feat.OrReqFeat2, prc_cached2da_feat.OrReqFeat3, prc_cached2da_feat.OrReqFeat4, 
            prc_cached2da_feat.REQSKILL, prc_cached2da_feat.REQSKILL2,
            prc_cached2da_feat.ReqMinSkillRanks, prc_cached2da_feat.ReqMinSkillRanks2
        FROM prc_cached2da_cls_feat INNER JOIN prc_cached2da_feat
        WHERE (prc_cached2da_feat.FEAT != '****') AND (prc_cached2da_cls_feat.FeatIndex != '****')
            AND (prc_cached2da_cls_feat.file = '<cls_feat***>')
            AND ((prc_cached2da_cls_feat.List = 1) OR (prc_cached2da_cls_feat.List = 2))
            AND (prc_cached2da_cls_feat.GrantedOnLevel <= 1)
            AND (prc_cached2da_feat.rowid = prc_cached2da_cls_feat.FeatIndex)
            AND (`PreReqEpic` != 1)
            AND (`MinLevel` = '****' OR `MinLevel` = '1')
            AND `ALLCLASSESCANUSE` = 0
            AND `minattackbonus` <= <nBAB>
            AND `minspelllvl` <= 1
            AND `minstr`<= <nStr>
            AND `mindex`<= <nDex>
            AND `mincon`<= <nCon>
            AND `minint`<= <nInt>
            AND `minwis`<= <nWis>
            AND `mincha`<= <nCha>
            AND `MinFortSave` <= <nFortSave>
        */

        sSQL = "SELECT prc_cached2da_feat.rowid, feat, prereqfeat1, prereqfeat2, "
                +"orreqfeat0, orreqfeat1, orreqfeat2, orreqfeat3, orreqfeat4, "
                +"reqskill, reqskill2, reqskillminranks, reqskillminranks2"
                +" FROM prc_cached2da_feat INNER JOIN prc_cached2da_cls_feat"
                +" WHERE (prc_cached2da_feat.feat != '****') AND (prc_cached2da_cls_feat.featindex != '****')"
                +" AND (prc_cached2da_cls_feat.file = '" + sFile + "')"
                +" AND ((prc_cached2da_cls_feat.list = 1) OR (prc_cached2da_cls_feat.list = 2))"
                +" AND (prc_cached2da_cls_feat.grantedonlevel <= 1)"
                +" AND (prc_cached2da_feat.rowid = prc_cached2da_cls_feat.featindex)"
                +" AND (prereqepic != 1)"
                +" AND (minlevel = '****' OR minlevel = '1')"
                +" AND (minattackbonus <= "+IntToString(nBAB)+")"
                +" AND (minspelllvl <= 1)"
                +" AND (minstr <= "+IntToString(nStr)+")"
                +" AND (mindex <= "+IntToString(nDex)+")"
                +" AND (mincon <= "+IntToString(nCon)+")"
                +" AND (minint <= "+IntToString(nInt)+")"
                +" AND (minwis <= "+IntToString(nWis)+")"
                +" AND (mincha <= "+IntToString(nCha)+")"
                +" AND (minfortsave <= "+IntToString(nFortSave)+")"
                +" LIMIT 5 OFFSET "+IntToString(nReali);

    // debug print the sql statement
    if(DEBUG)
    {
        DoDebug(sSQL);
    }

    PRC_SQLExecDirect(sSQL);
    // to keep track of where in the 25 rows we stop getting a result
    int nCounter = 0;
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        int nRow = StringToInt(PRC_SQLGetData(1));
        int nStrRef = StringToInt(PRC_SQLGetData(2));
        string sName = GetStringByStrRef(nStrRef);
        string sPreReqFeat1 = PRC_SQLGetData(3);
        string sPreReqFeat2 = PRC_SQLGetData(4);
        string sOrReqFeat0 = PRC_SQLGetData(5);
        string sOrReqFeat1 = PRC_SQLGetData(6);
        string sOrReqFeat2 = PRC_SQLGetData(7);
        string sOrReqFeat3 = PRC_SQLGetData(8);
        string sOrReqFeat4 = PRC_SQLGetData(9);
        string sReqSkill = PRC_SQLGetData(10);
        string sReqSkill2 = PRC_SQLGetData(11);
        string sReqSkillRanks = PRC_SQLGetData(12);
        string sReqSkillRanks2 = PRC_SQLGetData(13);

        // check AND feat prerequisites
        if (GetMeetsANDPreReq(sPreReqFeat1, sPreReqFeat2))
        {
            // check OR feat prerequisites
            if (GetMeetsORPreReq(sOrReqFeat0, sOrReqFeat1, sOrReqFeat2, sOrReqFeat3, sOrReqFeat4))
            {
                // check skill prerequisites
                if(GetMeetSkillPrereq(sReqSkill, sReqSkill2, sReqSkillRanks, sReqSkillRanks2))
                {
                    // check they don't have it already
                    if(!PreReqFeatArrayLoop(nRow))
                    {
                        // check it's not a psion discipline
                        if (nClass != CLASS_TYPE_PSION || !(nRow >= 3554 && nRow <= 3559))
                            AddChoice(sName, nRow);
                    }
                    else
                    {
                        if(DEBUG) DoDebug("Already picked feat " + IntToString(nRow) + ". Not added!");
                    }
                }
                else
                {
                    if(DEBUG) DoDebug("Not met skill prereq for feat " + IntToString(nRow) + ". Not added!");
                }
            }
            else
            {
                if(DEBUG) DoDebug("Not met OR prereqfeat test for feat " + IntToString(nRow) + ". Not added!");
            }
        }
        else
        {
            if(DEBUG) DoDebug("Not met AND prereqfeat test for feat " + IntToString(nRow) + ". Not added!");
        }
    } // end of while(PRC_SQLFetch() == PRC_SQL_SUCCESS)

    if(nCounter == 5)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+5);
        DelayCommand(0.01, DoBonusFeatLoop());
    }
    else // there were less than 5 rows, it's the end of the 2da
    {
        AddChoicesFromCache();
        if(DEBUG) DoDebug("Finished bonus feats");
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
        DeleteLocalInt(OBJECT_SELF, "i");
        return;
    }
}

void DoSpellsLoop(int nStage)
{
    // get which spell level the choices are for
    int nSpellLevel = 0;
    if (nStage == STAGE_SPELLS_1)
        nSpellLevel = 1;
    int nClass = GetLocalInt(OBJECT_SELF, "Class");
    string sSQL;
    // get the results 30 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    switch(nClass)
    {
        case CLASS_TYPE_WIZARD: {
            int nSpellSchool = GetLocalInt(OBJECT_SELF, "School");
            string sOpposition = Get2DACache("spellschools", "letter", StringToInt(Get2DACache("spellschools", "opposition", nSpellSchool)));
            sSQL = "SELECT rowid, name FROM prc_cached2da_spells WHERE (name != '****') AND (wiz_sorc = '1') AND (school != '"+sOpposition+"') LIMIT 30 OFFSET "+IntToString(nReali);
            break;
        }
        case CLASS_TYPE_SORCERER: {
            sSQL = "SELECT rowid, name FROM prc_cached2da_spells WHERE (name != '****') AND(wiz_sorc = '"+IntToString(nSpellLevel)+"') LIMIT 30 OFFSET "+IntToString(nReali);
            break;
        }
        case CLASS_TYPE_BARD: {
            sSQL = "SELECT rowid, name FROM prc_cached2da_spells WHERE (name != '****') AND(bard = '"+IntToString(nSpellLevel)+"') LIMIT 30 OFFSET "+IntToString(nReali);
            break;
        }
    }
    
    PRC_SQLExecDirect(sSQL);
    // to keep track of where in the 10 rows we stop getting a result
    int nCounter = 0;
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        // has it already been chosen?
        int nSpell = StringToInt(PRC_SQLGetData(1));
        // if they don't know the spell, add it to the choice list
        if(!GetIsSpellKnown(nSpell, nSpellLevel))
        {
            string sName = GetStringByStrRef(StringToInt(PRC_SQLGetData(2)));
            AddChoice(sName, nSpell);
        }
    } // end of while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    
    if (nCounter == 30)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+30);
        DelayCommand(0.01, DoSpellsLoop(nStage));
    }
    else // end of the 2da
    {
        if(DEBUG) DoDebug("Finished spells");
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "i");
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
        return;
    }
}

void  DoDomainsLoop()
{
    int i = 0;
    string sName;
    // get the first domain chosen if it's there
    int nDomain = GetLocalInt(OBJECT_SELF, "Domain1");
    
    // genasi elemental domain enforcement only is needed on the first domain
    if (!nDomain && GetPRCSwitch(PRC_CONVOCC_GENASI_ENFORCE_DOMAINS))
    {
        // now check PC race
        int nRace = GetLocalInt(OBJECT_SELF, "Race");
        if(nRace == RACIAL_TYPE_AIR_GEN)
            i = DOMAIN_AIR;
        else if(nRace == RACIAL_TYPE_EARTH_GEN)
            i = DOMAIN_EARTH;
        else if(nRace == RACIAL_TYPE_FIRE_GEN)
            i = DOMAIN_FIRE;
        else if(nRace == RACIAL_TYPE_WATER_GEN)
            i = DOMAIN_WATER;
    }
    // see if i was just set
    if (i) // if set, then the player gets no choice
    {
        i--; // the domain constants are offset by 1 to their 2da lines
        sName = Get2DACache("domains", "Name", i);
        AddChoice(GetStringByStrRef(StringToInt(sName)), i);
    }
    else // give them the full domain list
    {
        // get the file end
        int nFileEnd = GetPRCSwitch(FILE_END_DOMAINS);
        /* SELECT statement
         * SELECT rowid, data FROM prc_cached2da
         * WHERE file = 'domains' AND columnid = 'name' AND data != '****' AND rowid >= <nFileEnd>
         */
         string sSQL = "SELECT rowid, data FROM prc_cached2da WHERE file = 'domains' AND columnid = 'name' AND data != '****' AND rowid <= " + IntToString(nFileEnd);
         PRC_SQLExecDirect(sSQL);
        
        // fix for air domain being 0
        // swap around -1 (actually air/0) and variable not set (0)
        if (nDomain == -1) // air domain
            nDomain = 0;
        else if (nDomain == 0) // not set
            nDomain = -1;
        int i;
        string sRowID;
        string sName;
         while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
         {
             i = StringToInt(PRC_SQLGetData(1)); // rowid
             sName = GetStringByStrRef(StringToInt(PRC_SQLGetData(2))); // data
             if (i != nDomain)
            {
                AddChoice(sName, i);
            }
         }
    }
    DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
    FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
}

void DoAppearanceLoop()
{
    // get the results 100 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    int nCounter = 0;
    string sSQL = "SELECT rowid, string_ref, label FROM prc_cached2da_appearance WHERE (label != '****') LIMIT 100 OFFSET "+IntToString(nReali);
    PRC_SQLExecDirect(sSQL);
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        string sName;
        int nStrRef = StringToInt(PRC_SQLGetData(2));
        if(nStrRef)
            sName = GetStringByStrRef(nStrRef);
        else
            sName = PRC_SQLGetData(3);
        int nRow = StringToInt(PRC_SQLGetData(1));
        AddChoice(sName, nRow);
    }
    if (nCounter == 100)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+100);
        DelayCommand(0.01, DoAppearanceLoop());
    }
    else // end of the 2da
    {
        if(DEBUG) DoDebug("Finished appearances");
        FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
        DeleteLocalInt(OBJECT_SELF, "i");
        DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
        return;
    }
}

void DoPortraitsLoop(int nGenderSort = TRUE)
{
    // get the results 100 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    int nCounter = 0;
    string sSQL;
    // get the gender and add it to the SQL statement
    string sGender = IntToString(GetLocalInt(OBJECT_SELF, "Gender"));
    // get the race for ordering
    string sRace;
    // map the race to appearance for the PC appearances
    // as eg. all drow portraits are labelled as elf portraits in bioware's portraits.2da
    int nFakeRace = MapAppearanceToRace(GetLocalInt(OBJECT_SELF, "Appearance"));
    if (nFakeRace != -1) // if the appearance was a default character model
        sRace = IntToString(nFakeRace);
    else // use the actual race
        sRace = IntToString(GetLocalInt(OBJECT_SELF, "Race"));
    
    // note: "BaseResRef != 'matron'" is because that portrait is referenced in bioware's
    // portraits.2da, but doesn't exist
    if (nGenderSort)
    {
        if(GetPRCSwitch(PRC_CONVOCC_USE_RACIAL_PORTRAIT)) // get only race specific ones
        {
            sSQL = "SELECT rowid, baseresref, FROM prc_cached2da_portraits WHERE (inanimatetype = '****') AND (baseresref != '****') AND (baseresref != 'matron') AND (sex = '" + sGender 
                + "') AND (race ='"+sRace+"') LIMIT 100 OFFSET "+IntToString(nReali);
        }
        else
        {
            // orders portraits of PC's race first, only PC's gender-specific portraits
            sSQL = "SELECT rowid, baseresref, CASE race WHEN "+ sRace +" THEN 0 else 1 END AS column1 FROM prc_cached2da_portraits WHERE (inanimatetype = '****') AND (baseresref != '****') AND (baseresref != 'matron') AND (sex = '" + sGender 
                + "') ORDER BY column1, race LIMIT 100 OFFSET "+IntToString(nReali);
        }
    }
    else
    {
        if(GetPRCSwitch(PRC_CONVOCC_USE_RACIAL_PORTRAIT)) // get only race specific ones
        {
            sSQL = "SELECT rowid, baseresref, FROM prc_cached2da_portraits WHERE (inanimatetype = '****') AND (baseresref != '****') AND (baseresref != 'matron') AND (sex != '" + sGender 
                + "') AND (race ='"+sRace+"') LIMIT 100 OFFSET "+IntToString(nReali);
        }
        else
        {
            sSQL = "SELECT rowid, baseresref FROM prc_cached2da_portraits WHERE (inanimatetype = '****') AND (baseresref != '****') AND (baseresref != 'matron') AND (sex != '" + sGender + "') LIMIT 100 OFFSET "+IntToString(nReali);
        }
    }
    PRC_SQLExecDirect(sSQL);
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        int nRow = StringToInt(PRC_SQLGetData(1));
        string sName = PRC_SQLGetData(2);
        AddChoice(sName, nRow);
    }
    if (nCounter == 100)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+100);
        DelayCommand(0.01, DoPortraitsLoop(nGenderSort));
    }
    else // end of the 2da
    {
        if (nGenderSort && !GetPRCSwitch(PRC_CONVOCC_RESTRICT_PORTRAIT_BY_SEX)) // run again to select the rest of the portraits
        {
            nGenderSort = FALSE;
            if(DEBUG) DoDebug("Finished gender specific portraits");
            DeleteLocalInt(OBJECT_SELF, "i");
            DelayCommand(0.01, DoPortraitsLoop(nGenderSort));
        }
        else // done
        {
            if(DEBUG) DoDebug("Finished portraits");
            FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
            DeleteLocalInt(OBJECT_SELF, "i");
            DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
            return;
        }
    }
}

void DoSoundsetLoop(int nGenderSort = TRUE)
{
    // get the results 100 rows at a time to avoid TMI
    int nReali = GetLocalInt(OBJECT_SELF, "i");
    int nCounter = 0;
    string sSQL;
    string sGender = IntToString(GetLocalInt(OBJECT_SELF, "Gender"));
    
    sSQL = "SELECT rowid, strref FROM prc_cached2da_soundset WHERE (resref != '****')";
    
    if(nGenderSort)
    {
        sSQL += " AND (gender = '" + sGender + "')";
    }
    else
    {
        sSQL += " AND (gender != '" + sGender + "')";
    }
    
    // make the SQL string
    if(GetPRCSwitch(PRC_CONVOCC_ONLY_PLAYER_VOICESETS))
        sSQL += " AND (TYPE = '0') LIMIT 100 OFFSET "+IntToString(nReali);
    else
        sSQL += " ORDER BY TYPE LIMIT 100 OFFSET "+IntToString(nReali);
    
    PRC_SQLExecDirect(sSQL);
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nCounter++;
        int nRow = StringToInt(PRC_SQLGetData(1));
        string sName = GetStringByStrRef(StringToInt(PRC_SQLGetData(2)));
        AddChoice(sName, nRow);
    }
    
    if (nCounter == 100)
    {
        SetLocalInt(OBJECT_SELF, "i", nReali+100);
        DelayCommand(0.01, DoSoundsetLoop());
    }
    else // end of the 2da
    {
        if (nGenderSort && !GetPRCSwitch(PRC_CONVOCC_RESTRICT_VOICESETS_BY_SEX)) // run again to select the rest of the voicesets
        {
            nGenderSort = FALSE;
            if(DEBUG) DoDebug("Finished gender specific voicesets");
            DeleteLocalInt(OBJECT_SELF, "i");
            DelayCommand(0.01, DoPortraitsLoop(nGenderSort));
        }
        else // done
        {
            if(DEBUG) DoDebug("Finished Soundsets");
            FloatingTextStringOnCreature("Done", OBJECT_SELF, FALSE);
            DeleteLocalInt(OBJECT_SELF, "i");
            DeleteLocalInt(OBJECT_SELF, "DynConv_Waiting");
            return;
        }
    }
}

void DoWingmodelLoop()
{
    if (GetPRCSwitch(PRC_CONVOCC_AVARIEL_WINGS) && GetLocalInt(OBJECT_SELF, "Race") == RACIAL_TYPE_AVARIEL)
        AddChoice("Bird", 6);
    else if (GetPRCSwitch(PRC_CONVOCC_FEYRI_WINGS) && GetLocalInt(OBJECT_SELF, "Race") == RACIAL_TYPE_FEYRI)
        AddChoice("Bat", 3);
    else if (GetPRCSwitch(PRC_CONVOCC_AASIMAR_WINGS) && GetLocalInt(OBJECT_SELF, "Race") == RACIAL_TYPE_AASIMAR)
    {
        AddChoice("None", 0);
        AddChoice("Angel", 2);
    }
    else if (GetPRCSwitch(PRC_CONVOCC_DISALLOW_CUSTOMISE_WINGS))
        AddChoice("None", 0);
    else
    {
        string sSQL;
        
        sSQL = "SELECT rowid, data FROM prc_cached2da WHERE file='wingmodel' AND columnid = 'label' AND data != '****'";
        
        PRC_SQLExecDirect(sSQL);
        while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
        {
            int nRow = StringToInt(PRC_SQLGetData(1));
            string sName = PRC_SQLGetData(2);
            AddChoice(sName, nRow);
        }
    }
}

void DoTailmodelLoop()
{
    if (GetPRCSwitch(PRC_CONVOCC_FEYRI_TAIL) && GetLocalInt(OBJECT_SELF, "Race") == RACIAL_TYPE_FEYRI)
        AddChoice("Devil", 3);
    else if (GetPRCSwitch(PRC_CONVOCC_TIEFLING_TAIL) && GetLocalInt(OBJECT_SELF, "Race") == RACIAL_TYPE_TIEFLING)
    {
        AddChoice("None", 0);
        AddChoice("Devil", 3);
    }
    else if (GetPRCSwitch(PRC_CONVOCC_DISALLOW_CUSTOMISE_TAIL))
        AddChoice("None", 0);
    else
    {
        string sSQL;
        
        sSQL = "SELECT rowid, data FROM prc_cached2da WHERE file='tailmodel' AND columnid = 'label' AND data != '****'";
        
        PRC_SQLExecDirect(sSQL);
        while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
        {
            int nRow = StringToInt(PRC_SQLGetData(1));
            string sName = PRC_SQLGetData(2);
            AddChoice(sName, nRow);
        }
    }
}

void AddRaceFeats(int nRace)
{
    // gets which race_feat***.2da to use
    string sFile = GetStringLowerCase(Get2DACache("racialtypes", "FeatsTable", nRace));
    // create the Feats array
    array_create(OBJECT_SELF, "Feats");
    int nQTMCount = 0;
    // add on any bonus feats from switches here
    nQTMCount += (GetPRCSwitch(PRC_CONVOCC_BONUS_FEATS));
    /*
        SELECT `data` FROM `prc_cached2da`
        WHERE (`file` = <race_feat***>) AND (`columnid` = 'FeatIndex') AND (`data` != '****')
    */
    string sSQL = "SELECT data FROM prc_cached2da WHERE (file = '" + sFile + "') AND (columnid = 'featindex') AND (data != '****')";
    PRC_SQLExecDirect(sSQL);
    int nFeat;
    while(PRC_SQLFetch() == PRC_SQL_SUCCESS)
    {
        nFeat = StringToInt(PRC_SQLGetData(1)); // feat index
        //alertness fix
        if(nFeat == 0)
            nFeat = -1;
        // if one of these is quick to master, mark for doing the bonus feat later
        if (nFeat == 258)
            nQTMCount++;
        array_set_int(OBJECT_SELF, "Feats", array_get_size(OBJECT_SELF, "Feats"), nFeat);
    }
    // set final bonus feat count
    SetLocalInt(OBJECT_SELF, "QTM", nQTMCount);
}

void AddClassFeats(int nClass)
{
    // gets which class_feat_***.2da to use
    string sFile = GetStringLowerCase(Get2DACache("classes", "FeatsTable", nClass));
    // Feats array should already exist, but check anyway
    int nArraySize = array_get_size(OBJECT_SELF, "Feats");
    if (nArraySize) // if there's stuff in there already
    {
        // has it's own table, so SQL is easiest
        // class feats granted at level 1
        /*
        SELECT `FeatIndex` FROM `prc_cached2da_cls_feat`
        WHERE (`file` = <cls_feat***>) AND (`List` = 3) AND (`GrantedOnLevel` = 1) AND (`FeatIndex` != '****')
        */
        string sSQL = "SELECT featindex FROM prc_cached2da_cls_feat WHERE (file = '" + sFile + "') AND (list = 3) AND (grantedonlevel = 1) AND (featindex != '****')";
        PRC_SQLExecDirect(sSQL);
        int nFeat;
        while (PRC_SQLFetch() == PRC_SQL_SUCCESS)
        {
            nFeat = StringToInt(PRC_SQLGetData(1)); // feat index
            //alertness fix
            if(nFeat == 0)
                nFeat = -1;
            array_set_int(OBJECT_SELF, "Feats", array_get_size(OBJECT_SELF, "Feats"), nFeat);
        }
    }
    else // no feat array - screw up
    {
        /* TODO - start again */
    }
    
}

void AddDomainFeats()
{
    int nDomain = GetLocalInt(OBJECT_SELF, "Domain1");
    // air domain fix
    if (nDomain == -1)
        nDomain = 0;
    // get feat
    string sFeat = Get2DACache("domains", "GrantedFeat", nDomain);
    // add to the feat array
    array_set_int(OBJECT_SELF, "Feats", array_get_size(OBJECT_SELF, "Feats"),
            StringToInt(sFeat));
    
    nDomain = GetLocalInt(OBJECT_SELF, "Domain2");
    // air domain fix
    if (nDomain == -1)
        nDomain = 0;
    sFeat = Get2DACache("domains", "GrantedFeat", nDomain);
    // add to the feat array
    array_set_int(OBJECT_SELF, "Feats", array_get_size(OBJECT_SELF, "Feats"),
            StringToInt(sFeat));
}

void AddColourChoices(int nStage, int nCategory)
{
    // get which 2da column to use
    string s2DAColumn;
    if (nStage == STAGE_SKIN_COLOUR_CHOICE)
        s2DAColumn = "skin";
    else if (nStage == STAGE_HAIR_COLOUR_CHOICE)
        s2DAColumn = "hair";
    else // it's one of the tattoo colour stages
        s2DAColumn = "cloth";
    
    // get which rows to loop through
    int nStart = 0;
    int nStop = 0;
    switch(nCategory) // the category determines which colours get listed
    {
        case 1:
            nStart = 0;
            nStop = 7;
            break;
        case 2:
            nStart = 8;
            nStop = 15;
            break;
        case 3:
            nStart = 16;
            nStop = 23;
            break;
        case 4:
            nStart = 24;
            nStop = 31;
            break;
        case 5:
            nStart = 32;
            nStop = 39;
            break;
        case 6:
            nStart = 40;
            nStop = 47;
            break;
        case 7:
            nStart = 48;
            nStop = 55;
            break;
        case 8: // new colours
            nStart = 56;
            nStop = 63;
            break;
        case 9:
            nStart = 64;
            nStop = 71;
            break;
        case 10:
            nStart = 72;
            nStop = 79;
            break;
        case 11:
            nStart = 80;
            nStop = 87;
            break;
        case 12:
            nStart = 88;
            nStop = 95;
            break;
        case 13:
            nStart = 96;
            nStop = 103;
            break;
        case 14:
            nStart = 104;
            nStop = 111;
            break;
        case 15:
            nStart = 112;
            nStop = 119;
            break;
        case 16:
            nStart = 120;
            nStop = 127;
            break;
        case 17:
            nStart = 128;
            nStop = 135;
            break;
        case 18:
            nStart = 136;
            nStop = 143;
            break;
        case 19:
            nStart = 144;
            nStop = 151;
            break;
        case 20:
            nStart = 152;
            nStop = 159;
            break;
        case 21:
            nStart = 160;
            nStop = 167;
            break;
        case 22:
            nStart = 168;
            nStop = 175;
    }
    // make the list
    int i = nStart;
    while (i <= nStop)
    {
        AddChoice(Get2DACache("colours", s2DAColumn, i), i);
        i++;
    }
}