(svn r13651) [NoAI] -Fix: SUSPENDED state can be STOPPING state too in some cases, which made debugging slightly harder .. don't 'cheat' like that, and split the ambiguous SUSPENDED into unambiguous SUSPENDED and STOPPING
/* $Id$ */
/** @file ai_threads.cpp handles the threading of AIs */
#include "../stdafx.h"
#include "../openttd.h"
#include "../variables.h"
#include "../debug.h"
#include "../thread.h"
#include "../fiber.hpp"
#include "../player_func.h"
#include "ai.h"
#include "ai_threads.h"
#include "api/ai_controller.hpp"
#include "api/ai_object.hpp"
#include <map>
class AIFiber : public SimpleCountedObject {
private:
typedef std::map<int, CCountedPtr<AIFiber> > Fibers;
/** Different states of the AI fiber. */
enum FiberState {
INITIALIZED, ///< The mutex and conditions are initialized
STARTING, ///< The fiber is just started
RUNNING, ///< We are inside the main AI
SUSPENDED, ///< We are suspended (either by sleep or block)
STOPPING, ///< We are being stopped
STOPPED, ///< The main AI has stopped
};
public:
/**
* Initialize this AIFiber for the given controller.
* @param player The PlayerID of this AI.
* @param controller The AI we have to run.
*/
AIFiber(PlayerID player, AIController *controller) :
fiber_id(player),
state(INITIALIZED),
ticks_to_sleep(0),
controller(controller),
fiber(NULL)
{
/* The player shouldn't have a fiber yet */
assert(s_fibers[player] == NULL);
/* Register the new fiber */
s_fibers[player] = this;
/* Ensure that main fiber has its wrapper too */
if (player != MAIN_FIBER) {
stFind(MAIN_FIBER);
} else {
this->fiber = Fiber::AttachCurrent(this);
assert(this->fiber->IsRunning());
}
}
~AIFiber()
{
/* Make sure we are not in the active list anymore */
assert(this->fiber_id == MAIN_FIBER || stFind(this->fiber_id) == NULL);
/* Clean up */
delete this->fiber;
}
/**
* Start the AI fiber and return once the AI calls Sleep or any other
* suspending command.
*/
bool Start()
{
/* Make sure we start fibers from the main fiber */
assert(stCurrentFiber() == stFind(MAIN_FIBER));
/* We shouldn't have a fiber yet */
assert(this->fiber == NULL);
/* And we shouldn't be starting the main fiber */
assert(fiber_id != MAIN_FIBER);
/* Create fiber for this AI */
this->fiber = Fiber::New(&stFiberProc, this);
if (!this->fiber->IsRunning()) return false;
/* Switch to STARTING mode */
this->state = SUSPENDED;
this->SwitchToState(STARTING);
return true;
}
/**
* Suspend this AI for a 'timeout' of period.
* @param timeout Time to suspend. < 0 means infinite (MultiPlayer only!)
* @note Only call this from within the fiber you want to execute the function on.
*/
void Suspend(int timeout)
{
/* Should not attempt to suspend main fiber */
assert(this->fiber_id != MAIN_FIBER);
/* AI fiber should be running */
assert(this->state == RUNNING);
/* We can only suspend ourself */
assert(this->stCurrentFiber() == this);
this->ticks_to_sleep = timeout;
/* When AI fiber gets suspended, it always switches back to main fiber */
AIFiber *main = stFind(MAIN_FIBER);
main->SwitchToState(RUNNING);
/* The main fiber wants this AI to die */
if (this->state == STOPPING) {
/* Exit the fiber */
this->Exit();
/* Here we will never come as... the fiber is dead */
assert(false);
return;
}
assert(this->state == RUNNING);
}
/**
* Set the AI fiber to resume at the next call of RunTick.
* @note Only call this from within the main fiber.
*/
void Resume()
{
/* Should be called from main fiber only */
assert(stCurrentFiber() == stFind(MAIN_FIBER));
/* AI fiber should be suspended */
assert(this->state == SUSPENDED);
/* Normally the ticks_to_sleep hangs at -1 for MP. Possible the MP is
* faster then the delay requested by the user. In this case the value
* is lower. To let the normal delay system kick in, we reverse the value
* of ticks_to_sleep. But now it doesn't directly continue when the value
* was 'hanging', so we subtract 1 and it all works fine. */
this->ticks_to_sleep = -this->ticks_to_sleep - 1;
}
/**
* Let the AI fiber run for a while and return when it is done.
* However, when the fiber is suspended and the suspend timeout has not yet
* passed, nothing happens except decrementing the before mentioned timeout.
* @note Only call this from within the main fiber.
*/
void RunTick()
{
/* If we already stopped, don't do anything */
if (this->state == STOPPING || this->state == STOPPED) return;
/* Should be called from main fiber only */
assert(stCurrentFiber() == stFind(MAIN_FIBER));
/* Only AI fibers should be resumed this way */
assert(this != stFind(MAIN_FIBER));
/* AI fiber should be still suspended */
assert(this->state == SUSPENDED);
this->controller->IncreaseTick();
/* If the value is < -1, the user wants a delay which might exceed the delay
* of the MP. Therefor we keep adding value to ticks_to_sleep till it
* reaches -1, and we 'hang' it there infinitely, until the MP commands
* comes in. In the case it happens sooner, the Resume() codes handles it
* nicely and makes the value positive with the remaining ticks to wait. */
if (this->ticks_to_sleep < -1) this->ticks_to_sleep++;
if (this->ticks_to_sleep < 0) return; // We have to wait infinitely
if (--this->ticks_to_sleep > 0) return; // We have to wait a little longer
/* Make the fiber running again */
this->SwitchToState(RUNNING);
}
/**
* Try to stop the AI, and mark it as such. You can cleanup the AI after
* this if you like; it won't be become active for sure.
* @note There is no guarantee the fiber dies or anything, we just promise
* it won't be called anymore.
* @note Only call this from within the main fiber.
*/
void Stop()
{
/* Should be called from main fiber only */
assert(stCurrentFiber() == stFind(MAIN_FIBER));
/* If we already stopped, don't do anything */
if (this->state == STOPPING || this->state == STOPPED) return;
/* Trigger a STOPPING round */
assert(this->state == SUSPENDED);
this->SwitchToState(STOPPING);
assert(this->state == STOPPING);
/* Mark the fiber as STOPPED */
this->state = STOPPED;
}
/**
* Find and return the fiber object by its 'id' (player_id or MAIN_FIBER).
* Returns NULL if such fiber object doesn't exist. If
* (fiber_id == MAIN_FIBER) and the fiber object with this id doesn't
* exist, the new one is created and attached to the current (main) fiber.
*/
static AIFiber *stFind(int fiber_id)
{
Fibers::iterator it = s_fibers.find(fiber_id);
if (it != s_fibers.end()) return (*it).second;
if (fiber_id != MAIN_FIBER) return NULL;
/* Main fiber doesn't have its own fiber object, create it */
return new AIFiber((PlayerID)MAIN_FIBER, NULL);
}
/**
* Remove fiber object from the collection by its 'id' and destroy it.
*/
static void stRelease(int fiber_id)
{
Fibers::iterator it = s_fibers.find(fiber_id);
if (it != s_fibers.end()) s_fibers.erase(it);
}
/**
* Find the fiber object that belongs to the currently running fiber (caller).
*/
static AIFiber *stCurrentFiber()
{
AIFiber *cur = (AIFiber *)Fiber::GetCurrentFiberData();
assert(cur != NULL);
return cur;
}
private:
/**
* Switch the state of a fiber. It becomes active and executed until it
* changes the state. The current active fiber becomes SUSPENDED.
* @note Only call this from an other fiber.
*/
void SwitchToState(FiberState new_state)
{
/* You can't switch the state of an already active fiber */
assert(stCurrentFiber() != this);
/* The fiber should be suspended */
assert(this->state == SUSPENDED);
/* And there should be an active fiber on it */
assert(this->fiber != NULL);
/* Suspend the fiber */
stCurrentFiber()->state = SUSPENDED;
/* Switch to new state, activate fiber, and check if we are suspended again */
this->state = new_state;
this->fiber->SwitchToFiber();
assert(this->state == SUSPENDED || this->state == STOPPING);
}
/**
* The method that runs in the new Fiber. Automaticly called when the Fiber becomes active.
*/
void FiberProc()
{
/* We should be starting */
assert(this->state == STARTING);
/* We should be the active thread */
assert(stCurrentFiber() == this);
/* Switch to running */
this->state = RUNNING;
/* We want to skip the first tick, so switch back to the main fiber */
AIFiber *main = stFind(MAIN_FIBER);
main->SwitchToState(RUNNING);
if (this->state != STOPPING) {
/* Start up the AI (this should be an infinite loop) */
this->controller->Start();
/* If we come here, the AI exited because it wanted to */
DEBUG(ai, 1, "We've got a suicidal AI for player %d", this->fiber_id);
/* Wait until we are killed nicely by the game */
while (this->state != STOPPING) {
main->SwitchToState(RUNNING);
}
}
/* Stopping, and exit */
this->state = STOPPING;
this->Exit();
/* Here we will never come as... the fiber is dead */
assert(false);
}
/**
* Function that is started as process of the new AI fiber.
*/
static void CDECL stFiberProc(void *fiber)
{
AIFiber *cur = (AIFiber *)fiber;
cur->FiberProc();
}
/**
* Exit a running fiber.
* @note Only call this from within the fiber you want to execute the function on.
*/
void Exit()
{
/* We can only exit if we are the thread */
assert(stCurrentFiber() == this);
/* We can never exit the main fiber */
assert(fiber_id != MAIN_FIBER);
this->fiber->Exit();
}
static Fibers s_fibers; ///< Collection of active fibers.
static const int MAIN_FIBER = -1;
int fiber_id; ///< id of this fiber (or MAIN_FIBER for the main).
FiberState state; ///< The current state of the AI.
int ticks_to_sleep; ///< Sleep this many runticks.
AIController *controller; ///< Controller of this AI.
Fiber *fiber; ///< Fiber we are connected to.
};
/* static */ AIFiber::Fibers AIFiber::s_fibers;
/**
* Suspend the AI handler of a player.
* @param player the player to suspend for
* @param timeout Time to suspend. < 0 means infinite (MultiPlayer only!)
* @note This should be called from within the fiber running this player!
*/
void AI_SuspendPlayer(PlayerID player, int timeout)
{
DEBUG(ai, 6, "AI_SuspendPlayer(%d, %d) from thr %d", player, timeout, ThreadObject::CurrentId());
AIFiber *thr = AIFiber::stCurrentFiber();
assert(thr != NULL);
thr->Suspend(timeout);
}
/**
* Run the fiber of an AI player. Return when it is suspended again.
* @param player the player to run this tick for
*/
void AI_RunTick(PlayerID player)
{
DEBUG(ai, 6, "AI_RunTick(%d) from thr %d", player, ThreadObject::CurrentId());
AIFiber *thr = AIFiber::stFind(player);
if (thr == NULL) {
DEBUG(ai, 0, "AI_RunTick() called for dead AI player #%d", player);
return;
}
thr->RunTick();
}
/**
* Run an AI player for the first time.
* @param player the (AI) player to start
* @param controller the actual AI
*/
void AI_StartPlayer(PlayerID player, AIController *controller)
{
DEBUG(ai, 6, "AI_StartPlayer(%d) from thr %d", player, ThreadObject::CurrentId());
AIFiber *thr = AIFiber::stFind(player);
assert(thr == NULL);
thr = new AIFiber(player, controller);
thr->Start();
}
/**
* Stops the given player
* @param player the (AI) player to stop
*/
void AI_StopPlayer(PlayerID player)
{
DEBUG(ai, 6, "AI_StopPlayer(%d) from thr %d", player, ThreadObject::CurrentId());
AIFiber *thr = AIFiber::stFind(player);
if (thr == NULL) return;
thr->Stop();
AIFiber::stRelease(player);
}
/**
* Callback for when a network command was executed.
* As commands are always ordered, we don't have to worry about right command.
*/
void CcAI(bool success, TileIndex tile, uint32 p1, uint32 p2)
{
DEBUG(ai, 6, "CcAI(%d) from thr %d", _current_player, ThreadObject::CurrentId());
AIFiber *thr = AIFiber::stFind(_current_player);
assert(thr != NULL);
/* Store if we were a success or not */
AIObject::SetLastCommandRes(success);
/* Resume the fiber now */
thr->Resume();
}