EDIT: Try xwarren's below first. I don't entirely know what I'm doing here, and he does, soooo....
There's also a little mistake in what I've written, but it isn't critical. If it works then I'll fix it, lol. (note to self: nRetVal is getting overwritten from ADDED)
It is unfortunately impossible for me to test anything before posting as I have never had the problems you're describing.
I won't pretend that I fully understand the PRC caching system, nor why the lookups aren't working for you. The fact that the script I gave you worked (which ignores the cache and reads the .2da files straight out the hakpaks) implies that your 2da cache is broken for some reason. From the reading I've just done it suggests that it is saved to the campaign database on a regular basis - the exact frequency (in seconds) is equal to the value of the prc switch PRC_USE_BIOWARE_DATABASE (which defaults to 300) multiplied by six - for a recache every half hour.
In theory then, it
should be possible to just change the cached values on the caching golem, then force a rewrite of the database. Whether this will work, or continue working forever is something I am unsure of. If weird things happen you should just be able to delete the PRC_DATA_* files in your NWN/database folder, and that should sort anything unexpected out.
The following is a result of modifying Get2DACache so that it will throw the given columns of any given .2da file into the cache. I can't really test it, so I've added a number of debugging options that might make things a little easier if it doesn't work as expected. If it does work (I'm probably getting ahead of myself here!) and you need to run it on another 2da file, then it would be a lot easier if you were to edit it yourself, which will mean that you'll have to recompile it. I did leave a way for you to operate it without doing this, but setting all the variables correctly from the debug console is going to be rather difficult.
As you already have a PRC module set up, you can do this by...
1) Get a community made compiler. Bioware's one in the toolset dislikes the quantity of constants and other things in the PRC scripts. There's a link to the download at the top of the page: PRC downloads -> PRC script compiler. That is a command line interface, so setting up a batch file to compile stuff with is the advisable option, something like this will do...
CD C:\NeverwinterNights\NWN\modules\temp0
C:\NeverwinterNights\NWN\utils\nwnnsscomp.exe -cego *.nss
CD ..
PAUSE
You'll need to change the paths to whatever they are on your system, but that should give you the right idea.
2) Add prc_include to your module's hak list. In the toolset, go edit->module properties->custom content, and add prc_include in anywhere. Don't click OK just yet, as the toolset decides to try and build the module after this - which involves compiling all scripts. As Bioware's compiler can't handle that, unless you stop it within a few seconds the toolset will crash. After clicking OK, the toolset will unpack the hakpaks as it does when you first open the module, and it will then throw up an interface with a cancel button. Sometimes it just isn't possible to be fast enough to hit cancel, if you can't seem to do it then there are other ways to get it in the list and happy, but unfortunately you'll need other software to do this.
3) Now, go tools->script editor, and paste the script in and save it as something. The Bioware compiler will then attempt to compile it and give you a lovely "unknown state in compiler" error, which is fine. Run the batch file (or otherwise compile your script) save your module and you should be good to go.
Anyway, here's the script that may or may not work...
#include "prc_alterations"
//These are for Force2DACache
const int FORCE_CACHE_CORRECT = 1;
const int FORCE_CACHE_ADDED = 2;
const int FORCE_CACHE_WRONG = 3;
const int FORCE_CACHE_SKIP_UNCACHED = 1;
//These are for main()
const string FORCE_CACHE_DEBUG_LEVEL = "2DACacherDebugLevel"; //Int: 0 = quiet, any other value = noisy. Default 0
const string FORCE_CACHE_RATE = "2DACacherRate"; //Int: Default and maximum 100. Number of lines processed per second.
const string FORCE_CACHE_MODE = "2DACacherMode"; //Int: Flags passed to the nMode parameter of Force2DACache. Default 0
const string FORCE_CACHE_FILE = "2DACacherFile"; //String: Default cls_spell_archv
const string FORCE_CACHE_COLUMNS = "2DACacherColumns"; //Array name where columns are set.
//Sets the value of the PRC cache so that it may retrieve the given data value correctly.
//Returns one of the following:
//FORCE_CACHE_CORRECT - 2DA cache was already correct
//FORCE_CACHE_ADDED - Value not previously cached
//FORCE_CACHE_WRONG - Value was previously wrong
//nMode can accept the following flags:
//FORCE_CACHE_SKIP_UNCACHED - if set, values not already in the cache will not be inserted. Will still return FORCE_CACHE_ADDED in such cases.
int Force2DACache(string s2DA, string sColumn, int nRow, int nMode=0);
void ForceBiowareDatabaseRecache();
//With that many arguments, making a mistake in code I can't test is too risky
//This cuts down the scope for wrong ordering rather a lot
struct CacheLoopArgs
{
int nDebug;
float fDelay;
int nFlags;
string sFile;
int bDeleteColumnArray;
int nCorrects;
int nAddeds;
int nWrongs;
int nBlanks;
int nLine;
};
void CacheLoop(struct CacheLoopArgs sArgs);
void main()
{
struct CacheLoopArgs sArgs;
int nRate = GetLocalInt(OBJECT_SELF, FORCE_CACHE_RATE);
sArgs.nDebug = GetLocalInt(OBJECT_SELF, FORCE_CACHE_DEBUG_LEVEL);
if (nRate == 0) { sArgs.fDelay = 0.01f; }
else
{
sArgs.fDelay = 1.0f/IntToFloat(nRate);
if (sArgs.fDelay < 0.01f) { sArgs.fDelay = 0.01f; }
}
sArgs.nFlags = GetLocalInt(OBJECT_SELF, FORCE_CACHE_MODE);
//This sets the columns/file to cache
sArgs.sFile = GetLocalString(OBJECT_SELF, FORCE_CACHE_FILE);
if (sArgs.sFile == "") { sArgs.sFile = "cls_spell_archv"; }
if (!array_exists(OBJECT_SELF, FORCE_CACHE_COLUMNS))
{
array_create(OBJECT_SELF, FORCE_CACHE_COLUMNS);
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 0, "Level");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 1, "FeatID");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 2, "IPFeatID");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 3, "SpellID");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 4, "RealSpellID");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 5, "ReqFeat");
array_set_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, 6, "AL");
sArgs.bDeleteColumnArray = TRUE; //Mark that array for deletion at the end
}
CacheLoop(sArgs);
}
void CacheLoop(struct CacheLoopArgs sArgs)
{
if (sArgs.nBlanks > 20)
{
//Probably EOF, this writes the output
string sMessage = sArgs.sFile + ": Finished caching ";
int i;
for (i=0; i<array_get_size(OBJECT_SELF, FORCE_CACHE_COLUMNS); i++)
{
sMessage += array_get_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, i) + ", ";
}
SendMessageToPC(OBJECT_SELF, IntToString(sArgs.nAddeds) + " values added to cache.");
SendMessageToPC(OBJECT_SELF, IntToString(sArgs.nWrongs) + " values corrected in cache.");
SendMessageToPC(OBJECT_SELF, IntToString(sArgs.nCorrects) + " values were already correct in cache.");
if (sArgs.bDeleteColumnArray)
{
array_delete(OBJECT_SELF, FORCE_CACHE_COLUMNS);
}
//Aaand force the database to recache
ForceBiowareDatabaseRecache();
SendMessageToPC(OBJECT_SELF, "Database should now be recached.");
return;
}
int i;
int bLineIsBlank = TRUE;
if (sArgs.nDebug)
{
SendMessageToPC(OBJECT_SELF, "Line " + IntToString(sArgs.nLine) + ":");
}
for (i=0; i<array_get_size(OBJECT_SELF, FORCE_CACHE_COLUMNS); i++)
{
string sColumn = array_get_string(OBJECT_SELF, FORCE_CACHE_COLUMNS, i);
//First, test for blank lines
if (bLineIsBlank)
{
string sReal = Get2DAString(sArgs.sFile, sColumn, sArgs.nLine);
if (sReal != "")
{
bLineIsBlank=FALSE;
sArgs.nBlanks = 0; //Set blank line counter to 0, if not already
}
}
//Do the actual caching and increment the variables
int nRet = Force2DACache(sArgs.sFile, sColumn, sArgs.nLine, sArgs.nFlags);
switch (nRet)
{
case FORCE_CACHE_CORRECT: sArgs.nCorrects++; break;
case FORCE_CACHE_ADDED: sArgs.nAddeds++; break;
case FORCE_CACHE_WRONG: sArgs.nWrongs++; break;
}
//If heavy debugging enabled, print it all out
if (sArgs.nDebug)
{
switch (nRet)
{
case FORCE_CACHE_CORRECT: SendMessageToPC(OBJECT_SELF, PRC_TEXT_LIGHT_BLUE + sColumn + " was correct."); break;
case FORCE_CACHE_ADDED: SendMessageToPC(OBJECT_SELF, PRC_TEXT_ORANGE + sColumn + " was added."); break;
case FORCE_CACHE_WRONG: SendMessageToPC(OBJECT_SELF, PRC_TEXT_RED + sColumn + " was wrong!"); break;
}
}
}
//Increment line, and keep going
sArgs.nLine++;
DelayCommand(sArgs.fDelay, CacheLoop(sArgs));
}
//Easy one first.
void ForceBiowareDatabaseRecache()
{
SetLocalInt(GetModule(), "Bioware2dacacheCount", GetPRCSwitch(PRC_USE_BIOWARE_DATABASE));
//Forcing it can't hurt - assuming the HB will fire is incorrect if it's not set as the module heartbeat for some weird reason
ExecuteScript("prc_onheartbeat", GetModule());
}
int Force2DACache(string s2DA, string sColumn, int nRow, int nMode=0)
{
//lower case the 2da and column
s2DA = GetStringLowerCase(s2DA);
sColumn = GetStringLowerCase(sColumn);
//get the chest that contains the cache
object oCacheWP = GetObjectByTag("Bioware2DACache");
//if no chest, use HEARTOFCHAOS in limbo as a location to make a new one
if (!GetIsObjectValid(oCacheWP))
{
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Cache container creature does not exist, creating new one");
//oCacheWP = CreateObject(OBJECT_TYPE_PLACEABLE, "plc_chest2",
// GetLocation(GetObjectByTag("HEARTOFCHAOS")), FALSE, "Bioware2DACache");
//has to be a creature, placeables cant go through the DB
oCacheWP = CreateObject(OBJECT_TYPE_CREATURE, "prc_2da_cache",
GetLocation(GetObjectByTag("HEARTOFCHAOS")), FALSE, "Bioware2DACache");
}
//get the token for this file
string sFileWPName = s2DA + "_" + sColumn + "_" + IntToString(nRow / 1000);
//if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Token tag is " + sFileWPName);
object oFileWP = GetObjectByTag(sFileWPName);
//token doesnt exist make it
//EVERYTHING above this point is taken directly from Get2DACache
//If the FORCE_CACHE_SKIP_UNCACHED flag is set, don't do any more as we don't want to
if (nMode & FORCE_CACHE_SKIP_UNCACHED)
{
return FORCE_CACHE_ADDED;
}
int nRetVal;
if(!GetIsObjectValid(oFileWP))
{
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Token does not exist, creating new one");
// Use containers to avoid running out of inventory space
int nContainer = 0;
string sContainerName = "Bio2DACacheTokenContainer_" + GetSubString(s2DA, 0, 1) + "_";
object oContainer = GetObjectByTag(sContainerName + IntToString(nContainer));
// Find last existing container
if(GetIsObjectValid(oContainer))
{
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Seeking last container in series: " + sContainerName);
// find the last container
nContainer = GetLocalInt(oContainer, "ContainerCount");
oContainer = GetObjectByTag(sContainerName + IntToString(nContainer));
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Found: " + DebugObject2Str(oContainer));
// Make sure it's not full
if(GetLocalInt(oContainer, "NumTokensInside") >= 34) // Container has 35 slots. Attempt to not use them all, just in case
{
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Container full: " + DebugObject2Str(oContainer));
oContainer = OBJECT_INVALID;
++nContainer; // new container is 1 higher than last one
}
}
// We need to create a container
if(!GetIsObjectValid(oContainer))
{
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Creating new container");
oContainer = CreateObject(OBJECT_TYPE_ITEM, "nw_it_contain001", GetLocation(oCacheWP), FALSE, sContainerName + IntToString(nContainer));
DestroyObject(oContainer);
oContainer = CopyObject(oContainer, GetLocation(oCacheWP), oCacheWP, sContainerName + IntToString(nContainer));
// store the new number of containers in this series
if (nContainer)
SetLocalInt( GetObjectByTag(sContainerName + "0"), "ContainerCount", nContainer);
// else this is the first container - do nothing as this is the same as storing 0 on it.
// Also here we still have 2 objects with the same tag so above code may get
// the object destroyed at the end of the function if this is the first container.
}
if(DEBUG_GET2DACACHE) DoDebug("Get2DACache: Using container: " + DebugObject2Str(oContainer));
// Create the new token
/*oFileWP = CreateObject(OBJECT_TYPE_ITEM, "hidetoken", GetLocation(oCacheWP), FALSE, sFileWPName);
DestroyObject(oFileWP);
oFileWP = CopyObject(oFileWP, GetLocation(oCacheWP), oCacheWP, sFileWPName);*/
oFileWP = CreateItemOnObject("hidetoken", oContainer, 1, sFileWPName);
//SetName(oFileWP, "2da Cache - " + sFileWPName);
// Increment token count tracking variable
SetLocalInt(oContainer, "NumTokensInside", GetLocalInt(oContainer, "NumTokensInside") + 1);
//Set return value to indicate that it was not already there
nRetVal = FORCE_CACHE_ADDED;
}
string sReal = Get2DAString(s2DA, sColumn, nRow);
//Convert to **** here, as that is what gets saved
if (sReal == "") { sReal = "****"; }
string sCurrent = GetLocalString(oFileWP, s2DA+"|"+sColumn+"|"+IntToString(nRow));
if (sReal == sCurrent)
{
return FORCE_CACHE_CORRECT;
}
else
{
SetLocalString(oFileWP, s2DA+"|"+sColumn+"|"+IntToString(nRow), sReal);
return FORCE_CACHE_WRONG;
}
}
You can change a few things about how this runs using the debug console. The commands "SetVarInt" and "SetVarString" allow you to set variables on objects. You can use them like this (They are case sensitive):
SetVarInt 2DACacherDebugLevel 1
The game will then prompt you to target that at something - you need to set it on your character. The variables supported by the script can be found towards the top of the source above. You CAN indeed change the file and the fields to cache using this, however as the fields are set in an array, the way you set them in-game is a little different. If you use "dm_dumplocals" on your character while the script is running, you should be able to find the array produced by the script - I haven't really paid much attention to how they are stored, but it should look something like this...
2DACacherColumns - 7 [INT]
2DACacherColumns_0 - "Level" [STR]
2DACacherColumns_1 - "FeatID" [STR]
2DACacherColumns_2 - "IPFeatID" [STR]
2DACacherColumns_3 - "SpellID" [STR]
2DACacherColumns_4 - "RealSpellID" [STR]
2DACacherColumns_5 - "ReqFeat" [STR]
2DACacherColumns_6 - "AL" [STR]
The first one (it may be a string, I honestly don't remember) signifies the length of the array. You'll need to change that to a more appropriate value if you want to run the script this way, or, as I said above, you could just recompile it all yourself. The values of an array are always stored as strings, regardless of whether they are numerical or not.
Hopefully this works, if it doesn't then I'm a little out of ideas I'm afraid.