Ok, the problem was traced back to running out of triggers. (Actually, it's more like running out of ScStubs, which Trigger derives from). In AIProc, it detected when the lab was free, and setup a time trigger that would start the next research in 50 ticks. During those 50 ticks, 13 time triggers were created, one for each time AIProc was called before the lab got put to use 50 ticks later by the first time trigger to execute. For each time trigger, two ScStub derived classes are created. If I remember right, one was the time trigger, and the other was a stub that handled calling the callback function in the DLL. This used up 26 ScStub entries in an array that can support a maximum of about 255. When each of those time triggers fired, it basically calls Disable(), since they were not set to repeat. They did NOT call Destroy(). If Destroy is not called, those ScStub entries are never freed, and eventually the table fills up, and you can't create anymore. Any attempt to create more just returns a Trigger stub with ID 255, which you can't do anything useful with. When that happens, Time Triggers are no longer created, and your callback that starts the next research stops being called.
If you use a Time Trigger for a one-shot event, you should probably keep track of the Trigger reference by storing it in that ScriptGlobal struct, and in the callback that it causes to fire, call trigger.Destroy(). Mind you, you should still avoid creating all those extra triggers meanwhile to start with. If you're ever low on triggers, creating that many at once could cause problems. At any rate, you'll still need to call Destroy to prevent eventual problems.
Alternatively, you can use a repeating time trigger, since in this case you actually do want to reuse the trigger, and have it repeat it's action. Then you don't need to Destroy it, and then Create it again. What you can do is setup the trigger in InitProc, or similar, and have it set to repeat. Then, in the start of the callback, you can check if the lab is busy, and if it is, just return from the callback without doing anything. There would be one slight difference this way though. Instead of starting the next research topic 50 ticks after the last one finishes, it will start the next topic on a multiple of 50 ticks after the last one finishes. This might be right away, or it might be up to 50 ticks later. It will in general not be exactly 50 ticks though.
Another solution for your situation, is don't change any code except where it creates the Time Trigger, and instead of delaying the call to your function by 50 ticks using the trigger, just call the function directly, and have it execute right away. As soon as one tech finishes, it will immediately start the next one.
if (!scriptGlobals.bLab.GetBusy() && !Player[0].HasTechnology(3004))
{
//Trigger &BasicResearchTrigger = CreateTimeTrigger(1,1,50,"BasRe");
BasRe();
}
You might also consider using Research Triggers. Those will fire exactly when a research topic completes. If you still want to delay by exactly 50 ticks, then you can use both a Research Trigger and a Time Trigger. When the topic you are researching completes, the Research Trigger callback can start the Time Trigger. When the Time Trigger fires, it can start the next research topic.
ScriptGlobal
{
// ...
Trigger researchTrigger;
Trigger researchTimeTrigger;
};
... (somehwere in InitProc)
scriptGlobal.researchTimeTrigger = CreateTimeTrigger(true, false, 50, "StartNextResearch");
... (new function)
SCRIPT_API void StartNextResearch()
{
int bFailed = true; // Default assumption
// Determine next research topic
int techID = ...;
int techIndex = GetTechNum(techID)
// Start the next research here ...
// If failed, abort here without adjusting further settings
// (Although, you can Destroy the research trigger if you want)
if (bFailed)
return; // Will try again in 50 ticks
// Disable the Time Trigger (It is auto reset to a 50 tick delay, that starts when it's next enabled)
scriptGlobal.researchTimeTrigger.Disable();
// Set the Research Trigger
if (scriptGlobal.researchTrigger.IsInitialized()) // Only destroy it if it's already in use
scriptGlobal.researchTrigger.Destroy();
scriptGlobal.researchTrigger = CreateResearchTrigger(true, true, techID, playerNum, "ResearchComplete");
}
SCRIPT_API void ResearchComplete()
{
// Start the next topic in 50 ticks
scriptGlobal.researchTimeTrigger.Enable();
}
This will wait 50 ticks, and then start the first research. Whenever a topic completes, it will wait 50 ticks before starting the next. If it is unable to start, because the lab is destroyed, or disabled, then it will keep checking if it can start a new topic every 50 ticks. Note that the Time Trigger is never destroyed. It is only Disabled and Enabled. The Research Trigger needs to update which topic it is watching, and so you need to recreate this one, which means you need to Destroy the old one.
The next part is to simplify how you specify the list of topics to research. You'll want to put them in an array of some kind.
int researchTopicList[] =
{
2701,
2702,
// ...
};
You might also consider using an Enum, so the list is easier to read. Since you're designing your own tech file, you'd also have to write your own enum to do this.
Also, you might want to have two research lists if you AI can be either Eden or Plymouth. Remember that they can get different techs, or have different costs for researching each tech. It would then make sense to define two arrays if your AI needs to be able to play as either.
int edenResearchTopicList[] =
{
// ...
};
int plymouthResearchTopicList[] =
{
// ...
};
Of course there is no need to stop there either. You can have more arrays if you want. Maybe you want an AI to have a random personality. Perhaps it's more war like one round, and another it's racing to launch a spacecraft, or just building up it's base.
Next you'll need a way of traversing the list. For this you can add another variable to the ScriptGlobal struct. One that keeps track of the research topic index.
ScriptGlobal
{
// ...
Trigger researchTrigger;
Trigger researchTimeTrigger;
int researchTopicIndex;
};
// In InitProc
scriptGlobal.researchTopicIndex = 0; // Initialize to start of list
Then, each time you start a new research (successfully), you need to increment that value.
SCRIPT_API void StartNextResearch()
{
int bFailed = true; // Default assumption
// Determine next research topic
int techID = researchTopicList[scriptGlobal.researchTopicIndex];
int techIndex = GetTechNum(techID)
// Start the next research here
if (techIndex != -1)
{
// Start the research (with as many scientists as possible)
int maxScientists = GetMaxScientists(techIndex);
int numAvailableScientists = GetNumAvailableScientists(playerNum);
int numScientists = Min(maxScientists, numAvailableScientists);
scriptGlobal.lab.Research(techIndex, numScientists);
bFailed = false;
}
//... Rest of function, as above
}
SCRIPT_API void ResearchComplete()
{
// Increment the topic index
scriptGlobal.researchTopicIndex++;
// Start the next topic in 50 ticks
scriptGlobal.researchTimeTrigger.Enable();
}
What's still left to do, is terminating the research cycle once the list is complete, and determining the max number of scientists that you can assign to each research topic. There is a way to read this value from the in memory structs, so I'll provide a function to do that. Then you don't need to specify it in a seperate list, which is also error prone, especially if the tech file is updated.
Note: This following code to retrieve the max number of scientists you can assign to a research topic is
untested.
int __declspec(naked) __stdcall GetMaxScientists(int techIndex)
{
__asm
{
MOV EAX, [ESP + 4]
MOV ECX, [(0x56C230+4)] // Load TechInfo*[]*
MOV EDX, [ECX + EAX * 4] // Load TechInfo* (of techIndex)
MOV EAX, [EDX + 0x14] // EAX has return value of TechInfo.maxScientists
RETN 4
}
}
Now, you just have to make sure to terminate the research system when the list is complete. First, you'll need to know how big your list of topics is. There is an easy way to get the compiler to figure this out.
int researchTopicList[] = ...
int numResearchTopics = sizeof(researchTopicList)/sizeof(researchTopicList[0]);
Now, when the topic is incremented to this value, you need to shutdown the trigger system so it doesn't walk off the end of your list.
SCRIPT_API void StartNextResearch()
{
if (scriptGlobal.researchTopicIndex >= numResearchTopics)
{
// Shutdown trigger system. All research has been completed.
if (scriptGlobal.researchTrigger.IsInitialized()) // Need to check this in case the list was empty (and good habit)
scriptGlobal.researchTrigger.Destroy(); // Don't need this anymore
scriptGlobal.researchTimeTrigger.Destroy(); // Don't need this anymore either
return; // Don't try to do anything more
}
// ... Rest of function, as above
}
Note that I chose to destroy the trigger when it tried to start a new topic, after the old one finished, rather than immediate after the last topic started. This is because that last topic might not finish, if say the lab is destroyed. We want it to be restartable. This is also why I chose to increment the topic index after the research had been completed. If some topic doesn't complete, we don't want to skip over it and trying researching the one after it. Also, if the list of topics is empty, I want to system to just exit gracefully. Hence why I put the termination condition at the start of StartNextResearch, rather than after the increment in ResearchComplete.
There are a few problems that I'll leave you to solve. First, what happens when a lab is destroyed? How will you restart things and continue with the research? (I built the code so that this will hopefully be easy to do). You also need to figure out how the system will work with the 3 different lab types. As I wrote it, it assumes a single lab type for all research topics. You could use 3 seperate lists, but that won't guarantee you don't start a research topic until you're finished the prerequisites. You could also check which lab type a topic needs (I can provide code to get this), and then see if the variable for that lab type is set. If it is, start the topic at that lab. That would make the research topics serialize nicely, and should prevent weird dependcy bugs, provided your list of topics is correct. It's probably a little less obvious how to do research in parallel this way though. See if you can find a solution.
Also, I'd like to include the following disclaimer. I haven't tried out any of this code. I can't even guarantee if it will compile. Try it and find out for yourself. I've also been editing it as I go along, so it might not even be consistent.