2011-01-12 13:29:00 +01:00
|
|
|
/* Copyright (C) 2011 Wildfire Games.
|
|
|
|
* This file is part of 0 A.D.
|
|
|
|
*
|
|
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "precompiled.h"
|
|
|
|
|
|
|
|
#include "simulation2/system/Component.h"
|
|
|
|
#include "ICmpAIManager.h"
|
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
#include "simulation2/MessageTypes.h"
|
|
|
|
|
2011-02-10 17:06:28 +01:00
|
|
|
#include "graphics/Terrain.h"
|
2011-01-12 13:29:00 +01:00
|
|
|
#include "lib/timer.h"
|
2011-02-10 17:06:28 +01:00
|
|
|
#include "lib/tex/tex.h"
|
2011-04-29 21:10:34 +02:00
|
|
|
#include "lib/allocators/shared_ptr.h"
|
2011-01-12 13:29:00 +01:00
|
|
|
#include "ps/CLogger.h"
|
|
|
|
#include "ps/Filesystem.h"
|
2011-02-10 17:06:28 +01:00
|
|
|
#include "ps/Util.h"
|
2011-01-12 13:29:00 +01:00
|
|
|
#include "simulation2/components/ICmpAIInterface.h"
|
|
|
|
#include "simulation2/components/ICmpCommandQueue.h"
|
2011-02-10 17:06:28 +01:00
|
|
|
#include "simulation2/components/ICmpObstructionManager.h"
|
2011-06-29 01:24:42 +02:00
|
|
|
#include "simulation2/components/ICmpRangeManager.h"
|
2011-01-12 13:29:00 +01:00
|
|
|
#include "simulation2/components/ICmpTemplateManager.h"
|
2011-10-16 04:55:58 +02:00
|
|
|
#include "simulation2/components/ICmpTerritoryManager.h"
|
2011-02-10 17:06:28 +01:00
|
|
|
#include "simulation2/helpers/Grid.h"
|
2011-01-12 13:29:00 +01:00
|
|
|
#include "simulation2/serialization/DebugSerializer.h"
|
|
|
|
#include "simulation2/serialization/StdDeserializer.h"
|
|
|
|
#include "simulation2/serialization/StdSerializer.h"
|
|
|
|
#include "simulation2/serialization/SerializeTemplates.h"
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @file Player AI interface.
|
|
|
|
*
|
|
|
|
* AI is primarily scripted, and the CCmpAIManager component defined here
|
|
|
|
* takes care of managing all the scripts.
|
|
|
|
*
|
|
|
|
* To avoid slow AI scripts causing jerky rendering, they are run in a background
|
|
|
|
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
|
|
|
|
* turn before returning their results (though preferably they shouldn't use nearly
|
|
|
|
* that much CPU).
|
|
|
|
*
|
|
|
|
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
|
|
|
|
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
|
|
|
|
* The AI scripts will then run asynchronously and return a list of commands to execute.
|
|
|
|
* Any attempts to read the command list (including indirectly via serialization)
|
|
|
|
* will block until it's actually completed, so the rest of the engine should avoid
|
|
|
|
* reading it for as long as possible.
|
|
|
|
*
|
2011-01-16 00:35:20 +01:00
|
|
|
* JS values are passed between the game and AI threads using ScriptInterface::StructuredClone.
|
|
|
|
*
|
2011-01-12 13:29:00 +01:00
|
|
|
* TODO: actually the thread isn't implemented yet, because performance hasn't been
|
|
|
|
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
|
|
|
|
* is designed to hopefully support threading when we want it.
|
|
|
|
*/
|
|
|
|
|
|
|
|
class CAIWorker
|
|
|
|
{
|
|
|
|
private:
|
2011-01-16 00:35:20 +01:00
|
|
|
class CAIPlayer
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-01-16 00:35:20 +01:00
|
|
|
NONCOPYABLE(CAIPlayer);
|
|
|
|
public:
|
|
|
|
CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player,
|
|
|
|
const shared_ptr<ScriptRuntime>& runtime, boost::rand48& rng) :
|
|
|
|
m_Worker(worker), m_AIName(aiName), m_Player(player), m_ScriptInterface("Engine", "AI", runtime)
|
|
|
|
{
|
|
|
|
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
m_ScriptInterface.ReplaceNondeterministicFunctions(rng);
|
|
|
|
|
|
|
|
m_ScriptInterface.RegisterFunction<void, std::wstring, CAIPlayer::IncludeModule>("IncludeModule");
|
|
|
|
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIPlayer::PostCommand>("PostCommand");
|
2011-02-10 17:06:28 +01:00
|
|
|
|
|
|
|
m_ScriptInterface.RegisterFunction<void, std::wstring, std::vector<u32>, u32, u32, u32, CAIPlayer::DumpImage>("DumpImage");
|
2011-01-16 00:35:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
~CAIPlayer()
|
|
|
|
{
|
|
|
|
// Clean up rooted objects before destroying their script context
|
|
|
|
m_Obj = CScriptValRooted();
|
2011-07-17 01:24:14 +02:00
|
|
|
m_Commands.clear();
|
2011-01-16 00:35:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
static void IncludeModule(void* cbdata, std::wstring name)
|
|
|
|
{
|
|
|
|
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
|
|
|
|
|
|
|
|
self->LoadScripts(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void PostCommand(void* cbdata, CScriptValRooted cmd)
|
|
|
|
{
|
|
|
|
CAIPlayer* self = static_cast<CAIPlayer*> (cbdata);
|
|
|
|
|
|
|
|
self->m_Commands.push_back(self->m_ScriptInterface.WriteStructuredClone(cmd.get()));
|
|
|
|
}
|
|
|
|
|
2011-02-10 17:06:28 +01:00
|
|
|
/**
|
|
|
|
* Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights).
|
|
|
|
*/
|
|
|
|
static void DumpImage(void* UNUSED(cbdata), std::wstring name, std::vector<u32> data, u32 w, u32 h, u32 max)
|
|
|
|
{
|
|
|
|
// TODO: this is totally not threadsafe.
|
|
|
|
|
|
|
|
VfsPath filename = L"screenshots/aidump/" + name;
|
|
|
|
|
|
|
|
if (data.size() != w*h)
|
|
|
|
{
|
|
|
|
debug_warn(L"DumpImage: data size doesn't match w*h");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (max == 0)
|
|
|
|
{
|
|
|
|
debug_warn(L"DumpImage: max must not be 0");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const size_t bpp = 8;
|
|
|
|
int flags = TEX_BOTTOM_UP|TEX_GREY;
|
|
|
|
|
|
|
|
const size_t img_size = w * h * bpp/8;
|
|
|
|
const size_t hdr_size = tex_hdr_size(filename);
|
2011-04-29 21:10:34 +02:00
|
|
|
shared_ptr<u8> buf;
|
|
|
|
AllocateAligned(buf, hdr_size+img_size, maxSectorSize);
|
2011-02-10 17:06:28 +01:00
|
|
|
Tex t;
|
|
|
|
if (tex_wrap(w, h, bpp, flags, buf, hdr_size, &t) < 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
u8* img = buf.get() + hdr_size;
|
|
|
|
for (size_t i = 0; i < data.size(); ++i)
|
2011-08-16 13:18:32 +02:00
|
|
|
img[i] = (u8)((data[i] * 255) / max);
|
2011-02-10 17:06:28 +01:00
|
|
|
|
|
|
|
tex_write(&t, filename);
|
|
|
|
tex_free(&t);
|
|
|
|
}
|
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
bool LoadScripts(const std::wstring& moduleName)
|
|
|
|
{
|
|
|
|
// Ignore modules that are already loaded
|
|
|
|
if (m_LoadedModules.find(moduleName) != m_LoadedModules.end())
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// Mark this as loaded, to prevent it recursively loading itself
|
|
|
|
m_LoadedModules.insert(moduleName);
|
|
|
|
|
|
|
|
// Load and execute *.js
|
|
|
|
VfsPaths pathnames;
|
2011-05-25 12:39:13 +02:00
|
|
|
vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames);
|
2011-01-16 00:35:20 +01:00
|
|
|
for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it)
|
|
|
|
{
|
|
|
|
if (!m_ScriptInterface.LoadGlobalScriptFile(*it))
|
|
|
|
{
|
2011-03-23 14:36:20 +01:00
|
|
|
LOGERROR(L"Failed to load script %ls", it->string().c_str());
|
2011-01-16 00:35:20 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool Initialise(bool callConstructor)
|
|
|
|
{
|
|
|
|
if (!LoadScripts(m_AIName))
|
|
|
|
return false;
|
|
|
|
|
2011-03-23 14:36:20 +01:00
|
|
|
OsPath path = L"simulation/ai/" + m_AIName + L"/data.json";
|
2011-01-16 00:35:20 +01:00
|
|
|
CScriptValRooted metadata = m_Worker.LoadMetadata(path);
|
|
|
|
if (metadata.uninitialised())
|
|
|
|
{
|
2011-03-23 14:36:20 +01:00
|
|
|
LOGERROR(L"Failed to create AI player: can't find %ls", path.string().c_str());
|
2011-01-16 00:35:20 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the constructor name from the metadata
|
|
|
|
std::string constructor;
|
|
|
|
if (!m_ScriptInterface.GetProperty(metadata.get(), "constructor", constructor))
|
|
|
|
{
|
2011-03-23 14:36:20 +01:00
|
|
|
LOGERROR(L"Failed to create AI player: %ls: missing 'constructor'", path.string().c_str());
|
2011-01-16 00:35:20 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the constructor function from the loaded scripts
|
|
|
|
CScriptVal ctor;
|
|
|
|
if (!m_ScriptInterface.GetProperty(m_ScriptInterface.GetGlobalObject(), constructor.c_str(), ctor)
|
|
|
|
|| ctor.undefined())
|
|
|
|
{
|
2011-03-23 14:36:20 +01:00
|
|
|
LOGERROR(L"Failed to create AI player: %ls: can't find constructor '%hs'", path.string().c_str(), constructor.c_str());
|
2011-01-16 00:35:20 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
CScriptVal obj;
|
|
|
|
|
|
|
|
if (callConstructor)
|
|
|
|
{
|
|
|
|
// Set up the data to pass as the constructor argument
|
|
|
|
CScriptVal settings;
|
|
|
|
m_ScriptInterface.Eval(L"({})", settings);
|
|
|
|
m_ScriptInterface.SetProperty(settings.get(), "player", m_Player, false);
|
|
|
|
m_ScriptInterface.SetProperty(settings.get(), "templates", m_Worker.m_EntityTemplates, false);
|
|
|
|
|
|
|
|
obj = m_ScriptInterface.CallConstructor(ctor.get(), settings.get());
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// For deserialization, we want to create the object with the correct prototype
|
|
|
|
// but don't want to actually run the constructor again
|
|
|
|
obj = m_ScriptInterface.NewObjectFromConstructor(ctor.get());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (obj.undefined())
|
|
|
|
{
|
2011-03-23 14:36:20 +01:00
|
|
|
LOGERROR(L"Failed to create AI player: %ls: error calling constructor '%hs'", path.string().c_str(), constructor.c_str());
|
2011-01-16 00:35:20 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_Obj = CScriptValRooted(m_ScriptInterface.GetContext(), obj);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Run(CScriptVal state)
|
|
|
|
{
|
|
|
|
m_Commands.clear();
|
|
|
|
m_ScriptInterface.CallFunctionVoid(m_Obj.get(), "HandleMessage", state);
|
|
|
|
}
|
|
|
|
|
|
|
|
CAIWorker& m_Worker;
|
|
|
|
std::wstring m_AIName;
|
|
|
|
player_id_t m_Player;
|
|
|
|
|
|
|
|
ScriptInterface m_ScriptInterface;
|
|
|
|
CScriptValRooted m_Obj;
|
|
|
|
std::vector<shared_ptr<ScriptInterface::StructuredClone> > m_Commands;
|
|
|
|
std::set<std::wstring> m_LoadedModules;
|
2011-01-12 13:29:00 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
public:
|
2011-01-16 00:35:20 +01:00
|
|
|
struct SCommandSets
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
player_id_t player;
|
2011-01-16 00:35:20 +01:00
|
|
|
std::vector<shared_ptr<ScriptInterface::StructuredClone> > commands;
|
2011-01-12 13:29:00 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
CAIWorker() :
|
2011-01-16 00:35:20 +01:00
|
|
|
m_ScriptRuntime(ScriptInterface::CreateRuntime()),
|
|
|
|
m_ScriptInterface("Engine", "AI", m_ScriptRuntime),
|
2011-03-03 01:16:14 +01:00
|
|
|
m_TurnNum(0),
|
2011-01-16 00:35:20 +01:00
|
|
|
m_CommandsComputed(true)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
|
|
|
|
|
|
|
|
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
|
|
|
|
m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG);
|
|
|
|
}
|
|
|
|
|
|
|
|
~CAIWorker()
|
|
|
|
{
|
|
|
|
// Clear rooted script values before destructing the script interface
|
|
|
|
m_EntityTemplates = CScriptValRooted();
|
|
|
|
m_PlayerMetadata.clear();
|
|
|
|
m_Players.clear();
|
2011-07-17 01:24:14 +02:00
|
|
|
m_GameState.reset();
|
2011-10-16 04:55:58 +02:00
|
|
|
m_PassabilityMapVal = CScriptValRooted();
|
|
|
|
m_TerritoryMapVal = CScriptValRooted();
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
bool AddPlayer(const std::wstring& aiName, player_id_t player, bool callConstructor)
|
|
|
|
{
|
2011-01-16 00:35:20 +01:00
|
|
|
shared_ptr<CAIPlayer> ai(new CAIPlayer(*this, aiName, player, m_ScriptRuntime, m_RNG));
|
|
|
|
if (!ai->Initialise(callConstructor))
|
2011-01-12 13:29:00 +01:00
|
|
|
return false;
|
|
|
|
|
|
|
|
m_Players.push_back(ai);
|
2011-01-16 00:35:20 +01:00
|
|
|
|
2011-01-12 13:29:00 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2011-10-16 04:55:58 +02:00
|
|
|
void StartComputation(const shared_ptr<ScriptInterface::StructuredClone>& gameState, const Grid<u16>& passabilityMap, const Grid<u8>& territoryMap, bool territoryMapDirty)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-04-30 15:01:45 +02:00
|
|
|
ENSURE(m_CommandsComputed);
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
m_GameState = gameState;
|
|
|
|
|
2011-10-16 04:55:58 +02:00
|
|
|
if (passabilityMap.m_DirtyID != m_PassabilityMap.m_DirtyID)
|
2011-03-03 01:16:14 +01:00
|
|
|
{
|
2011-10-16 04:55:58 +02:00
|
|
|
m_PassabilityMap = passabilityMap;
|
2011-03-03 01:16:14 +01:00
|
|
|
|
|
|
|
JSContext* cx = m_ScriptInterface.GetContext();
|
2011-10-16 04:55:58 +02:00
|
|
|
m_PassabilityMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, m_PassabilityMap));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (territoryMapDirty)
|
|
|
|
{
|
|
|
|
m_TerritoryMap = territoryMap;
|
|
|
|
|
|
|
|
JSContext* cx = m_ScriptInterface.GetContext();
|
|
|
|
m_TerritoryMapVal = CScriptValRooted(cx, ScriptInterface::ToJSVal(cx, m_TerritoryMap));
|
2011-03-03 01:16:14 +01:00
|
|
|
}
|
|
|
|
|
2011-01-12 13:29:00 +01:00
|
|
|
m_CommandsComputed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void WaitToFinishComputation()
|
|
|
|
{
|
|
|
|
if (!m_CommandsComputed)
|
|
|
|
{
|
|
|
|
PerformComputation();
|
|
|
|
m_CommandsComputed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
void GetCommands(std::vector<SCommandSets>& commands)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
WaitToFinishComputation();
|
|
|
|
|
|
|
|
commands.clear();
|
2011-01-16 00:35:20 +01:00
|
|
|
commands.resize(m_Players.size());
|
|
|
|
for (size_t i = 0; i < m_Players.size(); ++i)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-01-16 00:35:20 +01:00
|
|
|
commands[i].player = m_Players[i]->m_Player;
|
|
|
|
commands[i].commands = m_Players[i]->m_Commands;
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void LoadEntityTemplates(const std::vector<std::pair<std::string, const CParamNode*> >& templates)
|
|
|
|
{
|
|
|
|
m_ScriptInterface.Eval("({})", m_EntityTemplates);
|
|
|
|
|
|
|
|
for (size_t i = 0; i < templates.size(); ++i)
|
|
|
|
{
|
|
|
|
jsval val = templates[i].second->ToJSVal(m_ScriptInterface.GetContext(), false);
|
|
|
|
m_ScriptInterface.SetProperty(m_EntityTemplates.get(), templates[i].first.c_str(), CScriptVal(val), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Since the template data is shared between AI players, freeze it
|
|
|
|
// to stop any of them changing it and confusing the other players
|
|
|
|
m_ScriptInterface.FreezeObject(m_EntityTemplates.get(), true);
|
|
|
|
}
|
|
|
|
|
|
|
|
void Serialize(std::ostream& stream, bool isDebug)
|
|
|
|
{
|
|
|
|
WaitToFinishComputation();
|
|
|
|
|
|
|
|
if (isDebug)
|
|
|
|
{
|
|
|
|
CDebugSerializer serializer(m_ScriptInterface, stream);
|
|
|
|
serializer.Indent(4);
|
|
|
|
SerializeState(serializer);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
CStdSerializer serializer(m_ScriptInterface, stream);
|
|
|
|
SerializeState(serializer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SerializeState(ISerializer& serializer)
|
|
|
|
{
|
2011-08-16 13:18:32 +02:00
|
|
|
serializer.NumberU32_Unbounded("num ais", (u32)m_Players.size());
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
for (size_t i = 0; i < m_Players.size(); ++i)
|
|
|
|
{
|
2011-01-16 00:35:20 +01:00
|
|
|
serializer.String("name", m_Players[i]->m_AIName, 0, 256);
|
|
|
|
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
|
|
|
|
serializer.ScriptVal("data", m_Players[i]->m_Obj);
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-08-16 13:18:32 +02:00
|
|
|
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
|
2011-01-16 00:35:20 +01:00
|
|
|
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
|
|
|
|
{
|
|
|
|
CScriptVal val = m_ScriptInterface.ReadStructuredClone(m_Players[i]->m_Commands[j]);
|
|
|
|
serializer.ScriptVal("command", val);
|
|
|
|
}
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Deserialize(std::istream& stream)
|
|
|
|
{
|
2011-04-30 15:01:45 +02:00
|
|
|
ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
CStdDeserializer deserializer(m_ScriptInterface, stream);
|
|
|
|
|
|
|
|
m_PlayerMetadata.clear();
|
|
|
|
m_Players.clear();
|
|
|
|
|
|
|
|
uint32_t numAis;
|
|
|
|
deserializer.NumberU32_Unbounded("num ais", numAis);
|
|
|
|
|
|
|
|
for (size_t i = 0; i < numAis; ++i)
|
|
|
|
{
|
|
|
|
std::wstring name;
|
|
|
|
player_id_t player;
|
|
|
|
deserializer.String("name", name, 0, 256);
|
|
|
|
deserializer.NumberI32_Unbounded("player", player);
|
|
|
|
if (!AddPlayer(name, player, false))
|
|
|
|
throw PSERROR_Deserialize_ScriptError();
|
|
|
|
|
|
|
|
// Use ScriptObjectAppend so we don't lose the carefully-constructed
|
|
|
|
// prototype/parent of this object
|
2011-01-16 00:35:20 +01:00
|
|
|
deserializer.ScriptObjectAppend("data", m_Players.back()->m_Obj.getRef());
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
uint32_t numCommands;
|
|
|
|
deserializer.NumberU32_Unbounded("num commands", numCommands);
|
|
|
|
m_Players.back()->m_Commands.reserve(numCommands);
|
|
|
|
for (size_t j = 0; j < numCommands; ++j)
|
|
|
|
{
|
|
|
|
CScriptVal val;
|
|
|
|
deserializer.ScriptVal("command", val);
|
|
|
|
m_Players.back()->m_Commands.push_back(m_ScriptInterface.WriteStructuredClone(val.get()));
|
|
|
|
}
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2011-03-21 23:59:00 +01:00
|
|
|
CScriptValRooted LoadMetadata(const VfsPath& path)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end())
|
|
|
|
{
|
|
|
|
// Load and cache the AI player metadata
|
|
|
|
m_PlayerMetadata[path] = m_ScriptInterface.ReadJSONFile(path);
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_PlayerMetadata[path];
|
|
|
|
}
|
|
|
|
|
|
|
|
void PerformComputation()
|
|
|
|
{
|
|
|
|
// Deserialize the game state, to pass to the AI's HandleMessage
|
2011-03-03 01:16:14 +01:00
|
|
|
CScriptVal state;
|
|
|
|
{
|
2011-11-04 02:35:50 +01:00
|
|
|
PROFILE3("AI compute read state");
|
2011-03-03 01:16:14 +01:00
|
|
|
state = m_ScriptInterface.ReadStructuredClone(m_GameState);
|
2011-10-16 04:55:58 +02:00
|
|
|
m_ScriptInterface.SetProperty(state.get(), "passabilityMap", m_PassabilityMapVal, true);
|
|
|
|
m_ScriptInterface.SetProperty(state.get(), "territoryMap", m_TerritoryMapVal, true);
|
2011-03-03 01:16:14 +01:00
|
|
|
}
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
// It would be nice to do
|
|
|
|
// m_ScriptInterface.FreezeObject(state.get(), true);
|
|
|
|
// to prevent AI scripts accidentally modifying the state and
|
|
|
|
// affecting other AI scripts they share it with. But the performance
|
|
|
|
// cost is far too high, so we won't do that.
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-11-04 02:35:50 +01:00
|
|
|
for (size_t i = 0; i < m_Players.size(); ++i)
|
2011-03-03 01:16:14 +01:00
|
|
|
{
|
2011-11-04 02:35:50 +01:00
|
|
|
PROFILE3("AI script");
|
|
|
|
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
|
|
|
|
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
|
|
|
|
m_Players[i]->Run(state);
|
2011-03-03 01:16:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run the GC every so often.
|
|
|
|
// (This isn't particularly necessary, but it makes profiling clearer
|
|
|
|
// since it avoids random GC delays while running other scripts)
|
|
|
|
if (m_TurnNum++ % 25 == 0)
|
|
|
|
{
|
2011-11-04 02:35:50 +01:00
|
|
|
PROFILE3("AI compute GC");
|
2011-03-03 01:16:14 +01:00
|
|
|
m_ScriptInterface.MaybeGC();
|
|
|
|
}
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
shared_ptr<ScriptRuntime> m_ScriptRuntime;
|
2011-01-12 13:29:00 +01:00
|
|
|
ScriptInterface m_ScriptInterface;
|
|
|
|
boost::rand48 m_RNG;
|
2011-03-03 01:16:14 +01:00
|
|
|
size_t m_TurnNum;
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
CScriptValRooted m_EntityTemplates;
|
2011-03-21 23:59:00 +01:00
|
|
|
std::map<VfsPath, CScriptValRooted> m_PlayerMetadata;
|
2011-01-16 00:35:20 +01:00
|
|
|
std::vector<shared_ptr<CAIPlayer> > m_Players; // use shared_ptr just to avoid copying
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
shared_ptr<ScriptInterface::StructuredClone> m_GameState;
|
2011-10-16 04:55:58 +02:00
|
|
|
Grid<u16> m_PassabilityMap;
|
|
|
|
CScriptValRooted m_PassabilityMapVal;
|
|
|
|
Grid<u8> m_TerritoryMap;
|
|
|
|
CScriptValRooted m_TerritoryMapVal;
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
bool m_CommandsComputed;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CCmpAIManager : public ICmpAIManager
|
|
|
|
{
|
|
|
|
public:
|
2011-03-05 02:56:59 +01:00
|
|
|
static void ClassInit(CComponentManager& componentManager)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-03-05 02:56:59 +01:00
|
|
|
componentManager.SubscribeToMessageType(MT_ProgressiveLoad);
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
DEFAULT_COMPONENT_ALLOCATOR(AIManager)
|
|
|
|
|
|
|
|
static std::string GetSchema()
|
|
|
|
{
|
|
|
|
return "<a:component type='system'/><empty/>";
|
|
|
|
}
|
|
|
|
|
2011-01-16 15:08:38 +01:00
|
|
|
virtual void Init(const CParamNode& UNUSED(paramNode))
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-11-23 22:24:41 +01:00
|
|
|
m_TerritoriesDirtyID = 0;
|
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
StartLoadEntityTemplates();
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
2011-01-16 15:08:38 +01:00
|
|
|
virtual void Deinit()
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void Serialize(ISerializer& serialize)
|
|
|
|
{
|
|
|
|
// Because the AI worker uses its own ScriptInterface, we can't use the
|
|
|
|
// ISerializer (which was initialised with the simulation ScriptInterface)
|
|
|
|
// directly. So we'll just grab the ISerializer's stream and write to it
|
|
|
|
// with an independent serializer.
|
|
|
|
|
2011-03-03 01:16:14 +01:00
|
|
|
// TODO: make the serialization/deserialization actually work, and not really slowly
|
|
|
|
// m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
|
|
|
|
UNUSED2(serialize);
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
2011-01-16 15:08:38 +01:00
|
|
|
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-01-16 15:08:38 +01:00
|
|
|
Init(paramNode);
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-03-03 01:16:14 +01:00
|
|
|
// m_Worker.Deserialize(deserialize.GetStream());
|
|
|
|
UNUSED2(deserialize);
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
|
|
|
|
{
|
|
|
|
switch (msg.GetType())
|
|
|
|
{
|
|
|
|
case MT_ProgressiveLoad:
|
|
|
|
{
|
|
|
|
const CMessageProgressiveLoad& msgData = static_cast<const CMessageProgressiveLoad&> (msg);
|
|
|
|
|
2011-08-16 13:18:32 +02:00
|
|
|
*msgData.total += (int)m_TemplateNames.size();
|
2011-03-05 02:56:59 +01:00
|
|
|
|
|
|
|
if (*msgData.progressed)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (ContinueLoadEntityTemplates())
|
|
|
|
*msgData.progressed = true;
|
|
|
|
|
2011-08-16 13:18:32 +02:00
|
|
|
*msgData.progress += (int)m_TemplateLoadedIdx;
|
2011-03-05 02:56:59 +01:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2011-06-29 01:24:42 +02:00
|
|
|
|
2011-01-12 13:29:00 +01:00
|
|
|
virtual void AddPlayer(std::wstring id, player_id_t player)
|
|
|
|
{
|
|
|
|
m_Worker.AddPlayer(id, player, true);
|
2011-06-29 01:24:42 +02:00
|
|
|
|
|
|
|
// AI players can cheat and see through FoW/SoD, since that greatly simplifies
|
|
|
|
// their implementation.
|
|
|
|
// (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD)
|
|
|
|
CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext(), SYSTEM_ENTITY);
|
|
|
|
if (!cmpRangeManager.null())
|
|
|
|
cmpRangeManager->SetLosRevealAll(player, true);
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
virtual void StartComputation()
|
|
|
|
{
|
|
|
|
PROFILE("AI setup");
|
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
ForceLoadEntityTemplates();
|
|
|
|
|
2011-01-12 13:29:00 +01:00
|
|
|
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
|
|
|
|
|
|
|
|
CmpPtr<ICmpAIInterface> cmpAIInterface(GetSimContext(), SYSTEM_ENTITY);
|
2011-04-30 15:01:45 +02:00
|
|
|
ENSURE(!cmpAIInterface.null());
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
// Get the game state from AIInterface
|
2011-01-12 13:29:00 +01:00
|
|
|
CScriptVal state = cmpAIInterface->GetRepresentation();
|
|
|
|
|
2011-10-16 04:55:58 +02:00
|
|
|
// Get the passability data
|
2011-03-03 01:16:14 +01:00
|
|
|
Grid<u16> dummyGrid;
|
2011-10-16 04:55:58 +02:00
|
|
|
const Grid<u16>* passabilityMap = &dummyGrid;
|
2011-03-03 01:16:14 +01:00
|
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
|
|
|
|
if (!cmpPathfinder.null())
|
2011-10-16 04:55:58 +02:00
|
|
|
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
|
|
|
|
|
|
|
|
// Get the territory data
|
|
|
|
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
|
|
|
|
bool territoryMapDirty = false;
|
|
|
|
Grid<u8> dummyGrid2;
|
|
|
|
const Grid<u8>* territoryMap = &dummyGrid2;
|
|
|
|
CmpPtr<ICmpTerritoryManager> cmpTerritoryManager(GetSimContext(), SYSTEM_ENTITY);
|
|
|
|
if (!cmpTerritoryManager.null() && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID))
|
|
|
|
{
|
|
|
|
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
|
|
|
|
territoryMapDirty = true;
|
|
|
|
}
|
2011-02-10 17:06:28 +01:00
|
|
|
|
|
|
|
LoadPathfinderClasses(state);
|
|
|
|
|
2011-10-16 04:55:58 +02:00
|
|
|
m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state.get()), *passabilityMap, *territoryMap, territoryMapDirty);
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
virtual void PushCommands()
|
|
|
|
{
|
|
|
|
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
|
|
|
|
|
2011-01-16 00:35:20 +01:00
|
|
|
std::vector<CAIWorker::SCommandSets> commands;
|
2011-01-12 13:29:00 +01:00
|
|
|
m_Worker.GetCommands(commands);
|
|
|
|
|
|
|
|
CmpPtr<ICmpCommandQueue> cmpCommandQueue(GetSimContext(), SYSTEM_ENTITY);
|
|
|
|
if (cmpCommandQueue.null())
|
|
|
|
return;
|
|
|
|
|
|
|
|
for (size_t i = 0; i < commands.size(); ++i)
|
|
|
|
{
|
|
|
|
for (size_t j = 0; j < commands[i].commands.size(); ++j)
|
|
|
|
{
|
2011-01-16 00:35:20 +01:00
|
|
|
cmpCommandQueue->PushLocalCommand(commands[i].player,
|
|
|
|
scriptInterface.ReadStructuredClone(commands[i].commands[j]));
|
2011-01-12 13:29:00 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2011-03-05 02:56:59 +01:00
|
|
|
std::vector<std::string> m_TemplateNames;
|
|
|
|
size_t m_TemplateLoadedIdx;
|
|
|
|
std::vector<std::pair<std::string, const CParamNode*> > m_Templates;
|
2011-10-16 04:55:58 +02:00
|
|
|
size_t m_TerritoriesDirtyID;
|
2011-03-05 02:56:59 +01:00
|
|
|
|
|
|
|
void StartLoadEntityTemplates()
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
2011-03-05 02:56:59 +01:00
|
|
|
CmpPtr<ICmpTemplateManager> cmpTemplateManager(GetSimContext(), SYSTEM_ENTITY);
|
2011-04-30 15:01:45 +02:00
|
|
|
ENSURE(!cmpTemplateManager.null());
|
2011-03-05 02:56:59 +01:00
|
|
|
|
|
|
|
m_TemplateNames = cmpTemplateManager->FindAllTemplates(false);
|
|
|
|
m_TemplateLoadedIdx = 0;
|
|
|
|
m_Templates.reserve(m_TemplateNames.size());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tries to load the next entity template. Returns true if we did some work.
|
|
|
|
bool ContinueLoadEntityTemplates()
|
|
|
|
{
|
|
|
|
if (m_TemplateLoadedIdx >= m_TemplateNames.size())
|
|
|
|
return false;
|
2011-01-12 13:29:00 +01:00
|
|
|
|
|
|
|
CmpPtr<ICmpTemplateManager> cmpTemplateManager(GetSimContext(), SYSTEM_ENTITY);
|
2011-04-30 15:01:45 +02:00
|
|
|
ENSURE(!cmpTemplateManager.null());
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(m_TemplateNames[m_TemplateLoadedIdx]);
|
|
|
|
if (node)
|
|
|
|
m_Templates.push_back(std::make_pair(m_TemplateNames[m_TemplateLoadedIdx], node));
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
m_TemplateLoadedIdx++;
|
2011-01-12 13:29:00 +01:00
|
|
|
|
2011-03-05 02:56:59 +01:00
|
|
|
// If this was the last template, send the data to the worker
|
|
|
|
if (m_TemplateLoadedIdx == m_TemplateNames.size())
|
|
|
|
m_Worker.LoadEntityTemplates(m_Templates);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ForceLoadEntityTemplates()
|
|
|
|
{
|
|
|
|
while (ContinueLoadEntityTemplates())
|
2011-01-12 13:29:00 +01:00
|
|
|
{
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-02-10 17:06:28 +01:00
|
|
|
void LoadPathfinderClasses(CScriptVal state)
|
|
|
|
{
|
|
|
|
CmpPtr<ICmpPathfinder> cmpPathfinder(GetSimContext(), SYSTEM_ENTITY);
|
|
|
|
if (cmpPathfinder.null())
|
|
|
|
return;
|
|
|
|
|
|
|
|
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
|
|
|
|
|
|
|
|
CScriptVal classesVal;
|
|
|
|
scriptInterface.Eval("({ pathfinderObstruction: 1, foundationObstruction: 2 })", classesVal);
|
|
|
|
|
|
|
|
std::map<std::string, ICmpPathfinder::pass_class_t> classes = cmpPathfinder->GetPassabilityClasses();
|
|
|
|
for (std::map<std::string, ICmpPathfinder::pass_class_t>::iterator it = classes.begin(); it != classes.end(); ++it)
|
|
|
|
scriptInterface.SetProperty(classesVal.get(), it->first.c_str(), it->second, true);
|
|
|
|
|
|
|
|
scriptInterface.SetProperty(state.get(), "passabilityClasses", classesVal, true);
|
|
|
|
}
|
|
|
|
|
2011-01-12 13:29:00 +01:00
|
|
|
CAIWorker m_Worker;
|
|
|
|
};
|
|
|
|
|
|
|
|
REGISTER_COMPONENT_TYPE(AIManager)
|