src/ai/ai_threads.cpp
author truebrain
Mon, 30 Jun 2008 10:26:40 +0000
branchnoai
changeset 11094 72be0534cd0f
parent 10978 13fd0364b2c6
permissions -rw-r--r--
(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();
}