Index: ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp
===================================================================
--- ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 7762)
+++ ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 7763)
@@ -1,357 +1,357 @@
/* Copyright (C) 2010 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 .
*/
#include "precompiled.h"
#include "ActorViewer.h"
#include "View.h"
#include "graphics/ColladaManager.h"
#include "graphics/Model.h"
#include "graphics/ObjectManager.h"
#include "graphics/Patch.h"
#include "graphics/SkeletonAnimManager.h"
#include "graphics/Terrain.h"
#include "graphics/TextureEntry.h"
#include "graphics/TextureManager.h"
#include "graphics/UnitManager.h"
#include "maths/MathUtil.h"
#include "ps/Font.h"
#include "ps/GameSetup/Config.h"
#include "ps/ProfileViewer.h"
#include "renderer/Renderer.h"
#include "renderer/Scene.h"
#include "renderer/SkyManager.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpPosition.h"
#include "simulation2/components/ICmpUnitMotion.h"
#include "simulation2/components/ICmpVisual.h"
struct ActorViewerImpl : public Scene
{
NONCOPYABLE(ActorViewerImpl);
public:
ActorViewerImpl()
: Entity(INVALID_ENTITY), Terrain(), ColladaManager(), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager),
UnitManager(), Simulation2(&UnitManager, &Terrain), ObjectManager(MeshManager, SkeletonAnimManager, Simulation2)
{
UnitManager.SetObjectManager(ObjectManager);
}
entity_id_t Entity;
CStrW CurrentUnitID;
CStrW CurrentUnitAnim;
float CurrentSpeed;
bool WalkEnabled;
bool GroundEnabled;
bool ShadowsEnabled;
SColor4ub Background;
CTerrain Terrain;
CColladaManager ColladaManager;
CMeshManager MeshManager;
CSkeletonAnimManager SkeletonAnimManager;
CObjectManager ObjectManager;
CUnitManager UnitManager;
CSimulation2 Simulation2;
// Simplistic implementation of the Scene interface
void EnumerateObjects(const CFrustum& frustum, SceneCollector* c)
{
if (GroundEnabled)
c->Submit(Terrain.GetPatch(0, 0));
Simulation2.RenderSubmit(*c, frustum, false);
}
};
ActorViewer::ActorViewer()
: m(*new ActorViewerImpl())
{
m.WalkEnabled = false;
m.GroundEnabled = true;
m.ShadowsEnabled = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS);
m.Background = SColor4ub(255, 255, 255, 255);
// Create a tiny empty piece of terrain, just so we can put shadows
// on it without having to think too hard
m.Terrain.Initialize(1, NULL);
CTextureEntry* tex = g_TexMan.FindTexture("whiteness");
if (tex)
{
CPatch* patch = m.Terrain.GetPatch(0, 0);
for (ssize_t i = 0; i < PATCH_SIZE; ++i)
{
for (ssize_t j = 0; j < PATCH_SIZE; ++j)
{
CMiniPatch& mp = patch->m_MiniPatches[i][j];
mp.Tex = tex;
mp.Priority = 0;
}
}
}
else
{
debug_warn(L"Failed to load whiteness texture");
}
// Start the simulation
m.Simulation2.LoadDefaultScripts();
m.Simulation2.ResetState();
}
ActorViewer::~ActorViewer()
{
delete &m;
}
CSimulation2* ActorViewer::GetSimulation2()
{
return &m.Simulation2;
}
entity_id_t ActorViewer::GetEntity()
{
return m.Entity;
}
void ActorViewer::UnloadObjects()
{
m.ObjectManager.UnloadObjects();
}
void ActorViewer::SetActor(const CStrW& name, const CStrW& animation)
{
bool needsAnimReload = false;
CStrW id = name;
// Recreate the entity, if we don't have one or if the new one is different
if (m.Entity == INVALID_ENTITY || id != m.CurrentUnitID)
{
// Delete the old entity (if any)
if (m.Entity != INVALID_ENTITY)
{
m.Simulation2.DestroyEntity(m.Entity);
m.Simulation2.FlushDestroyedEntities();
m.Entity = INVALID_ENTITY;
}
// If there's no actor to display, return with nothing loaded
if (id.empty())
return;
m.Entity = m.Simulation2.AddEntity(L"preview|" + id);
if (m.Entity == INVALID_ENTITY)
return;
CmpPtr cmpPosition(m.Simulation2, m.Entity);
if (!cmpPosition.null())
{
cmpPosition->JumpTo(entity_pos_t::FromInt(CELL_SIZE*PATCH_SIZE/2), entity_pos_t::FromInt(CELL_SIZE*PATCH_SIZE/2));
cmpPosition->SetYRotation(entity_angle_t::FromFloat((float)M_PI));
}
needsAnimReload = true;
}
if (animation != m.CurrentUnitAnim)
needsAnimReload = true;
if (needsAnimReload)
{
CStr anim = CStr(animation).LowerCase();
// Emulate the typical simulation animation behaviour
float speed;
float repeattime = 0.f;
if (anim == "walk")
{
CmpPtr cmpUnitMotion(m.Simulation2, m.Entity);
if (!cmpUnitMotion.null())
- speed = cmpUnitMotion->GetSpeed().ToFloat();
+ speed = cmpUnitMotion->GetWalkSpeed().ToFloat();
else
speed = 7.f; // typical unit speed
m.CurrentSpeed = speed;
}
else if (anim == "run")
{
CmpPtr cmpUnitMotion(m.Simulation2, m.Entity);
if (!cmpUnitMotion.null())
speed = cmpUnitMotion->GetRunSpeed().ToFloat();
else
speed = 12.f; // typical unit speed
m.CurrentSpeed = speed;
}
else if (anim == "melee")
{
speed = 1.f; // speed will be ignored if we have a repeattime
m.CurrentSpeed = 0.f;
CStr code = "var cmp = Engine.QueryInterface("+CStr(m.Entity)+", IID_Attack); if (cmp) cmp.GetTimers(cmp.GetBestAttack()).repeat; else 0;";
m.Simulation2.GetScriptInterface().Eval(code.c_str(), repeattime);
}
else
{
// Play the animation at normal speed, but movement speed is zero
speed = 1.f;
m.CurrentSpeed = 0.f;
}
CStr sound;
if (anim == "melee")
sound = "attack";
else if (anim == "build")
sound = "build";
else if (anim.Find("gather_") == 0)
sound = anim;
std::wstring soundgroup;
if (!sound.empty())
{
CStr code = "var cmp = Engine.QueryInterface("+CStr(m.Entity)+", IID_Sound); if (cmp) cmp.GetSoundGroup('"+sound+"'); else '';";
m.Simulation2.GetScriptInterface().Eval(code.c_str(), soundgroup);
}
CmpPtr cmpVisual(m.Simulation2, m.Entity);
if (!cmpVisual.null())
{
// TODO: SetEntitySelection(anim)
cmpVisual->SelectAnimation(anim, false, speed, soundgroup);
if (repeattime)
cmpVisual->SetAnimationSyncRepeat(repeattime);
}
}
m.CurrentUnitID = id;
m.CurrentUnitAnim = animation;
}
void ActorViewer::SetBackgroundColour(const SColor4ub& colour)
{
m.Background = colour;
m.Terrain.SetBaseColour(colour);
}
void ActorViewer::SetWalkEnabled(bool enabled) { m.WalkEnabled = enabled; }
void ActorViewer::SetGroundEnabled(bool enabled) { m.GroundEnabled = enabled; }
void ActorViewer::SetShadowsEnabled(bool enabled) { m.ShadowsEnabled = enabled; }
void ActorViewer::SetStatsEnabled(bool enabled)
{
if (enabled)
g_ProfileViewer.ShowTable("renderer");
else
g_ProfileViewer.ShowTable("");
}
void ActorViewer::Render()
{
m.Terrain.MakeDirty(RENDERDATA_UPDATE_COLOR);
g_Renderer.SetClearColor(m.Background);
// Disable shadows locally (avoid clobbering global state)
bool oldShadows = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS);
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, m.ShadowsEnabled);
bool oldSky = g_Renderer.GetSkyManager()->m_RenderSky;
g_Renderer.GetSkyManager()->m_RenderSky = false;
g_Renderer.BeginFrame();
// Find the centre of the interesting region, in the middle of the patch
// and half way up the model (assuming there is one)
CVector3D centre;
CmpPtr cmpVisual(m.Simulation2, m.Entity);
if (!cmpVisual.null())
cmpVisual->GetBounds().GetCentre(centre);
else
centre.Y = 0.f;
centre.X = centre.Z = CELL_SIZE * PATCH_SIZE/2;
CCamera camera = View::GetView_Actor()->GetCamera();
camera.m_Orientation.Translate(centre.X, centre.Y, centre.Z);
camera.UpdateFrustum();
g_Renderer.SetSceneCamera(camera, camera);
g_Renderer.RenderScene(&m);
// ....
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glPushAttrib(GL_ENABLE_BIT);
glEnable(GL_TEXTURE_2D);
glDisable(GL_CULL_FACE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glDisable(GL_ALPHA_TEST);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_TEXTURE_2D);
g_ProfileViewer.RenderProfile();
glPopAttrib();
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
g_Renderer.EndFrame();
// Restore the old renderer state
g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, oldShadows);
g_Renderer.GetSkyManager()->m_RenderSky = oldSky;
ogl_WarnIfError();
}
void ActorViewer::Update(float dt)
{
m.Simulation2.Update((int)(dt*1000));
m.Simulation2.Interpolate(dt, 0);
if (m.WalkEnabled && m.CurrentSpeed)
{
CmpPtr cmpPosition(m.Simulation2, m.Entity);
if (!cmpPosition.null())
{
// Move the model by speed*dt forwards
float z = cmpPosition->GetPosition().Z.ToFloat();
z -= m.CurrentSpeed*dt;
// Wrap at the edges, so it doesn't run off into the horizon
if (z < CELL_SIZE*PATCH_SIZE * 0.4f)
z = CELL_SIZE*PATCH_SIZE * 0.6f;
cmpPosition->JumpTo(cmpPosition->GetPosition().X, entity_pos_t::FromFloat(z));
}
}
}
Index: ps/trunk/source/simulation2/MessageTypes.h
===================================================================
--- ps/trunk/source/simulation2/MessageTypes.h (revision 7762)
+++ ps/trunk/source/simulation2/MessageTypes.h (revision 7763)
@@ -1,178 +1,195 @@
/* Copyright (C) 2010 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 .
*/
#ifndef INCLUDED_MESSAGETYPES
#define INCLUDED_MESSAGETYPES
#include "simulation2/system/Components.h"
#include "simulation2/system/Entity.h"
#include "simulation2/system/Message.h"
#include "simulation2/helpers/Position.h"
#define DEFAULT_MESSAGE_IMPL(name) \
- virtual EMessageTypeId GetType() const { return MT_##name; } \
+ virtual int GetType() const { return MT_##name; } \
virtual const char* GetScriptHandlerName() const { return "On" #name; } \
virtual const char* GetScriptGlobalHandlerName() const { return "OnGlobal" #name; } \
virtual jsval ToJSVal(ScriptInterface& scriptInterface) const; \
static CMessage* FromJSVal(ScriptInterface&, jsval val);
class SceneCollector;
class CFrustum;
class CMessageTurnStart : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(TurnStart)
CMessageTurnStart()
{
}
};
class CMessageUpdate : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Update)
CMessageUpdate(fixed turnLength) :
turnLength(turnLength)
{
}
fixed turnLength;
};
class CMessageInterpolate : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Interpolate)
CMessageInterpolate(float frameTime, float offset) :
frameTime(frameTime), offset(offset)
{
}
float frameTime; // time in seconds since previous interpolate
float offset; // range [0, 1] (inclusive); fractional time of current frame between previous/next simulation turns
};
class CMessageRenderSubmit : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(RenderSubmit)
CMessageRenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) :
collector(collector), frustum(frustum), culling(culling)
{
}
SceneCollector& collector;
const CFrustum& frustum;
bool culling;
};
/**
+ * This is send immediately after a new entity's components have all be created
+ * and initialised.
+ */
+class CMessageCreate : public CMessage
+{
+public:
+ DEFAULT_MESSAGE_IMPL(Create)
+
+ CMessageCreate(entity_id_t entity) :
+ entity(entity)
+ {
+ }
+
+ entity_id_t entity;
+};
+
+/**
* This is sent immediately before a destroyed entity is flushed and really destroyed.
* (That is, after CComponentManager::DestroyComponentsSoon and inside FlushDestroyedComponents).
* The entity will still exist at the time this message is sent.
* It's possible for this message to be sent multiple times for one entity, but all its components
* will have been deleted after the first time.
*/
class CMessageDestroy : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(Destroy)
CMessageDestroy(entity_id_t entity) :
entity(entity)
{
}
entity_id_t entity;
};
class CMessageOwnershipChanged : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(OwnershipChanged)
CMessageOwnershipChanged(entity_id_t entity, int32_t from, int32_t to) :
entity(entity), from(from), to(to)
{
}
entity_id_t entity;
int32_t from;
int32_t to;
};
/**
* Sent during TurnStart.
*
* If @c inWorld is false, then the other fields are invalid and meaningless.
* Otherwise they represent the current position.
*/
class CMessagePositionChanged : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(PositionChanged)
CMessagePositionChanged(bool inWorld, entity_pos_t x, entity_pos_t z, entity_angle_t a) :
inWorld(inWorld), x(x), z(z), a(a)
{
}
bool inWorld;
entity_pos_t x, z;
entity_angle_t a;
};
/**
* Sent by CCmpUnitMotion during Update, whenever the motion status has changed
* since the previous update.
*/
class CMessageMotionChanged : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(MotionChanged)
CMessageMotionChanged(fixed speed) :
speed(speed)
{
}
fixed speed; // metres per second, or 0 if not moving
};
/**
* Sent when terrain (texture or elevation) has been changed.
*/
class CMessageTerrainChanged : public CMessage
{
public:
DEFAULT_MESSAGE_IMPL(TerrainChanged)
CMessageTerrainChanged(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) :
i0(i0), j0(j0), i1(i1), j1(j1)
{
}
ssize_t i0, j0, i1, j1; // inclusive lower bound, exclusive upper bound, in tiles
};
#endif // INCLUDED_MESSAGETYPES
Index: ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp
===================================================================
--- ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp (revision 7762)
+++ ps/trunk/source/simulation2/scripting/MessageTypeConversions.cpp (revision 7763)
@@ -1,203 +1,219 @@
/* Copyright (C) 2010 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 .
*/
#include "precompiled.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/MessageTypes.h"
#include "js/jsapi.h"
#define TOJSVAL_SETUP() \
ScriptInterface::LocalRootScope scope(scriptInterface.GetContext()); \
if (! scope.OK()) \
return JSVAL_VOID; \
JSObject* obj = JS_NewObject(scriptInterface.GetContext(), NULL, NULL, NULL); \
if (! obj) \
return JSVAL_VOID
#define SET_MSG_PROPERTY(name) \
do { \
jsval prop = ScriptInterface::ToJSVal(scriptInterface.GetContext(), this->name); \
if (! JS_SetProperty(scriptInterface.GetContext(), obj, #name, &prop)) \
return JSVAL_VOID; \
} while (0)
#define FROMJSVAL_SETUP() \
if (! JSVAL_IS_OBJECT(val)) \
return NULL; \
JSObject* obj = JSVAL_TO_OBJECT(val)
jsval prop;
#define GET_MSG_PROPERTY(type, name) \
if (! JS_GetProperty(scriptInterface.GetContext(), obj, #name, &prop)) \
return NULL; \
type name; \
if (! ScriptInterface::FromJSVal(scriptInterface.GetContext(), prop, name)) \
return NULL;
////////////////////////////////
jsval CMessageTurnStart::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessageTurnStart::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return new CMessageTurnStart();
}
////////////////////////////////
jsval CMessageUpdate::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(turnLength);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageUpdate::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(fixed, turnLength);
return new CMessageUpdate(turnLength);
}
////////////////////////////////
jsval CMessageInterpolate::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(frameTime);
SET_MSG_PROPERTY(offset);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageInterpolate::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(float, frameTime);
GET_MSG_PROPERTY(float, offset);
return new CMessageInterpolate(frameTime, offset);
}
////////////////////////////////
jsval CMessageRenderSubmit::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessageRenderSubmit::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return NULL;
}
////////////////////////////////
+jsval CMessageCreate::ToJSVal(ScriptInterface& scriptInterface) const
+{
+ TOJSVAL_SETUP();
+ SET_MSG_PROPERTY(entity);
+ return OBJECT_TO_JSVAL(obj);
+}
+
+CMessage* CMessageCreate::FromJSVal(ScriptInterface& scriptInterface, jsval val)
+{
+ FROMJSVAL_SETUP();
+ GET_MSG_PROPERTY(entity_id_t, entity);
+ return new CMessageCreate(entity);
+}
+
+////////////////////////////////
+
jsval CMessageDestroy::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(entity);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageDestroy::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(entity_id_t, entity);
return new CMessageDestroy(entity);
}
////////////////////////////////
jsval CMessageOwnershipChanged::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(entity);
SET_MSG_PROPERTY(from);
SET_MSG_PROPERTY(to);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageOwnershipChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(entity_id_t, entity);
GET_MSG_PROPERTY(int32_t, from);
GET_MSG_PROPERTY(int32_t, to);
return new CMessageOwnershipChanged(entity, from, to);
}
////////////////////////////////
jsval CMessagePositionChanged::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessagePositionChanged::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return NULL;
}
////////////////////////////////
jsval CMessageMotionChanged::ToJSVal(ScriptInterface& scriptInterface) const
{
TOJSVAL_SETUP();
SET_MSG_PROPERTY(speed);
return OBJECT_TO_JSVAL(obj);
}
CMessage* CMessageMotionChanged::FromJSVal(ScriptInterface& scriptInterface, jsval val)
{
FROMJSVAL_SETUP();
GET_MSG_PROPERTY(fixed, speed);
return new CMessageMotionChanged(speed);
}
////////////////////////////////
jsval CMessageTerrainChanged::ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const
{
return JSVAL_VOID;
}
CMessage* CMessageTerrainChanged::FromJSVal(ScriptInterface& UNUSED(scriptInterface), jsval UNUSED(val))
{
return NULL;
}
////////////////////////////////////////////////////////////////
CMessage* CMessageFromJSVal(int mtid, ScriptInterface& scriptingInterface, jsval val)
{
switch (mtid)
{
#define MESSAGE(name) case MT_##name: return CMessage##name::FromJSVal(scriptingInterface, val);
#define INTERFACE(name)
#define COMPONENT(name)
#include "simulation2/TypeList.h"
#undef COMPONENT
#undef INTERFACE
#undef MESSAGE
}
return NULL;
}
Index: ps/trunk/source/simulation2/TypeList.h
===================================================================
--- ps/trunk/source/simulation2/TypeList.h (revision 7762)
+++ ps/trunk/source/simulation2/TypeList.h (revision 7763)
@@ -1,113 +1,114 @@
/* Copyright (C) 2010 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 .
*/
// MESSAGE: message types
// INTERFACE: component interface types
// COMPONENT: component types
// Components intended only for use in test cases:
// (The tests rely on the enum IDs, so don't change the order of these)
INTERFACE(Test1)
COMPONENT(Test1A)
COMPONENT(Test1B)
COMPONENT(Test1Scripted)
INTERFACE(Test2)
COMPONENT(Test2A)
COMPONENT(Test2Scripted)
// Message types:
MESSAGE(TurnStart)
MESSAGE(Update)
MESSAGE(Interpolate) // non-deterministic (use with caution)
MESSAGE(RenderSubmit) // non-deterministic (use with caution)
+MESSAGE(Create)
MESSAGE(Destroy)
MESSAGE(OwnershipChanged)
MESSAGE(PositionChanged)
MESSAGE(MotionChanged)
MESSAGE(TerrainChanged)
// TemplateManager must come before all other (non-test) components,
// so that it is the first to be (de)serialized
INTERFACE(TemplateManager)
COMPONENT(TemplateManager)
// Special component for script component types with no native interface
INTERFACE(UnknownScript)
COMPONENT(UnknownScript)
// In alphabetical order:
INTERFACE(CommandQueue)
COMPONENT(CommandQueue)
INTERFACE(Footprint)
COMPONENT(Footprint)
INTERFACE(GuiInterface)
COMPONENT(GuiInterfaceScripted)
INTERFACE(Minimap)
COMPONENT(Minimap)
INTERFACE(Motion)
COMPONENT(MotionBall)
COMPONENT(MotionScripted)
INTERFACE(Obstruction)
COMPONENT(Obstruction)
INTERFACE(ObstructionManager)
COMPONENT(ObstructionManager)
INTERFACE(Ownership)
COMPONENT(Ownership)
INTERFACE(Pathfinder)
COMPONENT(Pathfinder)
INTERFACE(Player)
COMPONENT(PlayerScripted)
INTERFACE(PlayerManager)
COMPONENT(PlayerManagerScripted)
INTERFACE(Position)
COMPONENT(Position) // must be before VisualActor
INTERFACE(ProjectileManager)
COMPONENT(ProjectileManager)
INTERFACE(Selectable)
COMPONENT(Selectable)
INTERFACE(SoundManager)
COMPONENT(SoundManager)
INTERFACE(StatusBars)
COMPONENT(StatusBars)
INTERFACE(Terrain)
COMPONENT(Terrain)
INTERFACE(UnitMotion)
COMPONENT(UnitMotion) // must be after Obstruction
INTERFACE(Visual)
COMPONENT(VisualActor)
INTERFACE(WaterManager)
COMPONENT(WaterManager)
Index: ps/trunk/source/simulation2/system/ComponentManager.h
===================================================================
--- ps/trunk/source/simulation2/system/ComponentManager.h (revision 7762)
+++ ps/trunk/source/simulation2/system/ComponentManager.h (revision 7763)
@@ -1,260 +1,264 @@
/* Copyright (C) 2010 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 .
*/
#ifndef INCLUDED_COMPONENTMANAGER
#define INCLUDED_COMPONENTMANAGER
#include "Entity.h"
#include "Components.h"
#include "scriptinterface/ScriptInterface.h"
#include
#include
class IComponent;
class CParamNode;
class CMessage;
class CSimContext;
class CComponentManager
{
NONCOPYABLE(CComponentManager);
public:
// We can't use EInterfaceId/etc directly, since scripts dynamically generate new IDs
// and casting arbitrary ints to enums is undefined behaviour, so use 'int' typedefs
typedef int InterfaceId;
typedef int ComponentTypeId;
typedef int MessageTypeId;
private:
// Component allocation types
typedef IComponent* (*AllocFunc)(ScriptInterface& scriptInterface, jsval ctor);
typedef void (*DeallocFunc)(IComponent*);
// ComponentTypes come in three types:
// Native: normal C++ component
// ScriptWrapper: C++ component that wraps a JS component implementation
// Script: a ScriptWrapper linked to a specific JS component implementation
enum EComponentTypeType
{
CT_Native,
CT_ScriptWrapper,
CT_Script
};
// Representation of a component type, to be used when instantiating components
struct ComponentType
{
EComponentTypeType type;
InterfaceId iid;
AllocFunc alloc;
DeallocFunc dealloc;
std::string name;
std::string schema; // RelaxNG fragment
jsval ctor; // only valid if type == CT_Script
};
public:
CComponentManager(CSimContext&, bool skipScriptFunctions = false);
~CComponentManager();
void LoadComponentTypes();
/**
* Load a script and execute it in a new function scope.
* @param filename VFS path to load
* @param hotload set to true if this script has been loaded before, and redefinitions of
* existing components should not be considered errors
*/
bool LoadScript(const std::wstring& filename, bool hotload = false);
void RegisterMessageType(MessageTypeId mtid, const char* name);
void RegisterComponentType(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
void RegisterComponentTypeScriptWrapper(InterfaceId, ComponentTypeId, AllocFunc, DeallocFunc, const char*, const std::string& schema);
/**
* Subscribe the current component type to the given message type.
* Each component's HandleMessage will be called on any BroadcastMessage of this message type,
* or on any PostMessage of this type targeted at the component's entity.
* Must only be called by a component type's ClassInit.
*/
void SubscribeToMessageType(MessageTypeId mtid);
/**
* Subscribe the current component type to all messages of the given message type.
* Each component's HandleMessage will be called on any BroadcastMessage or PostMessage of this message type,
* regardless of the entity.
* Must only be called by a component type's ClassInit.
*/
void SubscribeGloballyToMessageType(MessageTypeId mtid);
/**
* @param cname Requested component type name (not including any "CID" or "CCmp" prefix)
* @return The component type id, or CID__Invalid if not found
*/
ComponentTypeId LookupCID(const std::string& cname) const;
/**
* @return The name of the given component type, or "" if not found
*/
std::string LookupComponentTypeName(ComponentTypeId cid) const;
/**
* Returns a new entity ID that has never been used before.
* This affects the simulation state so it must only be called in network-synchronised ways.
*/
entity_id_t AllocateNewEntity();
/**
* Returns a new local entity ID that has never been used before.
* This entity will not be synchronised over the network, stored in saved games, etc.
*/
entity_id_t AllocateNewLocalEntity();
/**
* Returns a new entity ID that has never been used before.
* If possible, returns preferredId, and ensures this ID won't be allocated again.
* This affects the simulation state so it must only be called in network-synchronised ways.
*/
entity_id_t AllocateNewEntity(entity_id_t preferredId);
/**
* Constructs a component of type 'cid', initialised with data 'paramNode',
* and attaches it to entity 'ent'.
*
* @return true on success; false on failure, and logs an error message
*/
bool AddComponent(entity_id_t ent, ComponentTypeId cid, const CParamNode& paramNode);
/**
* Adds an externally-created component, so that it is returned by QueryInterface
* but does not get destroyed and does not receive messages from the component manager.
* (This is intended for unit tests that need to add mock objects the tested components
* expect to exist.)
*/
void AddMockComponent(entity_id_t ent, InterfaceId iid, IComponent& component);
/**
* Allocates a component object of type 'cid', and attaches it to entity 'ent'.
* (The component's Init is not called here - either Init or Deserialize must be called
* before using the returned object.)
*/
IComponent* ConstructComponent(entity_id_t ent, ComponentTypeId cid);
/**
* Constructs an entity based on the given template, and adds it the world with
* entity ID @p ent. There should not be any existing components with that entity ID.
* @return ent, or INVALID_ENTITY on error
*/
entity_id_t AddEntity(const std::wstring& templateName, entity_id_t ent);
/**
* Destroys all the components belonging to the specified entity when FlushDestroyedComponents is called.
* Has no effect if the entity does not exist, or has already been added to the destruction queue.
*/
void DestroyComponentsSoon(entity_id_t ent);
/**
* Does the actual destruction of components from DestroyComponentsSoon.
* This must not be called if the component manager is on the call stack (since it
* will break internal iterators).
*/
void FlushDestroyedComponents();
IComponent* QueryInterface(entity_id_t ent, InterfaceId iid) const;
const std::map& GetEntitiesWithInterface(InterfaceId iid) const;
/**
* Send a message, targeted at a particular entity. The message will be received by any
* components of that entity which subscribed to the message type, and by any other components
* that subscribed globally to the message type.
*/
void PostMessage(entity_id_t ent, const CMessage& msg) const;
/**
* Send a message, not targeted at any particular entity. The message will be received by any
* components that subscribed (either globally or not) to the message type.
*/
void BroadcastMessage(const CMessage& msg) const;
/**
* Resets the dynamic simulation state (deletes all entities, resets entity ID counters;
* doesn't unload/reload component scripts).
*/
void ResetState();
// Various state serialization functions:
bool ComputeStateHash(std::string& outHash);
bool DumpDebugState(std::ostream& stream);
// FlushDestroyedComponents must be called before SerializeState (since the destruction queue
// won't get serialized)
bool SerializeState(std::ostream& stream);
bool DeserializeState(std::istream& stream);
std::string GenerateSchema();
ScriptInterface& GetScriptInterface() { return m_ScriptInterface; }
private:
// Implementations of functions exposed to scripts
static void Script_RegisterComponentType(void* cbdata, int iid, std::string cname, CScriptVal ctor);
static void Script_RegisterInterface(void* cbdata, std::string name);
+ static void Script_RegisterMessageType(void* cbdata, std::string name);
static void Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value);
static IComponent* Script_QueryInterface(void* cbdata, int ent, int iid);
static void Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data);
static void Script_BroadcastMessage(void* cbdata, int mtid, CScriptVal data);
static int Script_AddEntity(void* cbdata, std::string templateName);
static int Script_AddLocalEntity(void* cbdata, std::string templateName);
static void Script_DestroyEntity(void* cbdata, int ent);
+ CMessage* ConstructMessage(int mtid, CScriptVal data);
void SendGlobalMessage(const CMessage& msg) const;
ComponentTypeId GetScriptWrapper(InterfaceId iid);
ScriptInterface m_ScriptInterface;
const CSimContext& m_SimContext;
ComponentTypeId m_CurrentComponent; // used when loading component types
bool m_CurrentlyHotloading;
// TODO: some of these should be vectors
std::map m_ComponentTypesById;
std::map > m_ComponentsByInterface;
std::map > m_ComponentsByTypeId;
std::map > m_LocalMessageSubscriptions;
std::map > m_GlobalMessageSubscriptions;
std::map m_ComponentTypeIdsByName;
std::map m_MessageTypeIdsByName;
+ std::map m_MessageTypeNamesById;
std::map m_InterfaceIdsByName;
+
// TODO: maintaining both ComponentsBy* is nasty; can we get rid of one,
// while keeping QueryInterface and PostMessage sufficiently efficient?
std::vector m_DestructionQueue;
ComponentTypeId m_NextScriptComponentTypeId;
entity_id_t m_NextEntityId;
entity_id_t m_NextLocalEntityId;
boost::rand48 m_RNG;
friend class TestComponentManager;
};
#endif // INCLUDED_COMPONENTMANAGER
Index: ps/trunk/source/simulation2/system/Message.h
===================================================================
--- ps/trunk/source/simulation2/system/Message.h (revision 7762)
+++ ps/trunk/source/simulation2/system/Message.h (revision 7763)
@@ -1,40 +1,40 @@
/* Copyright (C) 2010 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 .
*/
#ifndef INCLUDED_MESSAGE
#define INCLUDED_MESSAGE
#include "scriptinterface/ScriptTypes.h"
class CMessage
{
NONCOPYABLE(CMessage);
protected:
CMessage() { }
public:
virtual ~CMessage() { }
- virtual EMessageTypeId GetType() const = 0;
+ virtual int GetType() const = 0;
virtual const char* GetScriptHandlerName() const = 0;
virtual const char* GetScriptGlobalHandlerName() const = 0;
virtual jsval ToJSVal(ScriptInterface&) const = 0;
};
// TODO: GetType could be replaced with a plain member variable to avoid some
// virtual calls, if that turns out to be worthwhile
CMessage* CMessageFromJSVal(int mtid, ScriptInterface&, jsval);
#endif // INCLUDED_MESSAGE
Index: ps/trunk/source/simulation2/system/ComponentManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 7762)
+++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 7763)
@@ -1,805 +1,870 @@
/* Copyright (C) 2010 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 .
*/
#include "precompiled.h"
#include "ComponentManager.h"
#include "IComponent.h"
#include "ParamNode.h"
#include "SimContext.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/MessageTypes.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
+/**
+ * Used for script-only message types.
+ */
+class CMessageScripted : public CMessage
+{
+public:
+ virtual int GetType() const { return mtid; }
+ virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); }
+ virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); }
+ virtual jsval ToJSVal(ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); }
+
+ CMessageScripted(int mtid, const std::string& name, const CScriptValRooted& msg) :
+ mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(msg)
+ {
+ }
+
+ int mtid;
+ std::string handlerName;
+ std::string globalHandlerName;
+ CScriptValRooted msg;
+};
+
CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFunctions) :
m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine"), m_SimContext(context), m_CurrentlyHotloading(false)
{
context.SetComponentManager(this);
m_ScriptInterface.SetCallbackData(static_cast (this));
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG);
// For component script tests, the test system sets up its own scripted implementation of
// these functions, so we skip registering them here in those cases
if (!skipScriptFunctions)
{
m_ScriptInterface.RegisterFunction ("RegisterComponentType");
m_ScriptInterface.RegisterFunction ("RegisterInterface");
+ m_ScriptInterface.RegisterFunction ("RegisterMessageType");
m_ScriptInterface.RegisterFunction ("RegisterGlobal");
m_ScriptInterface.RegisterFunction ("QueryInterface");
m_ScriptInterface.RegisterFunction ("PostMessage");
m_ScriptInterface.RegisterFunction ("BroadcastMessage");
m_ScriptInterface.RegisterFunction ("AddEntity");
m_ScriptInterface.RegisterFunction ("AddLocalEntity");
m_ScriptInterface.RegisterFunction ("DestroyEntity");
}
// Define MT_*, IID_* as script globals, and store their names
#define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name);
#define INTERFACE(name) \
m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \
m_InterfaceIdsByName[#name] = IID_##name;
#define COMPONENT(name)
#include "simulation2/TypeList.h"
#undef MESSAGE
#undef INTERFACE
#undef COMPONENT
m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY);
ResetState();
}
CComponentManager::~CComponentManager()
{
ResetState();
// Release GC roots
std::map::iterator it = m_ComponentTypesById.begin();
for (; it != m_ComponentTypesById.end(); ++it)
if (it->second.type == CT_Script)
m_ScriptInterface.RemoveRoot(&it->second.ctor);
}
void CComponentManager::LoadComponentTypes()
{
#define MESSAGE(name) \
RegisterMessageType(MT_##name, #name);
#define INTERFACE(name) \
extern void RegisterComponentInterface_##name(ScriptInterface&); \
RegisterComponentInterface_##name(m_ScriptInterface);
#define COMPONENT(name) \
extern void RegisterComponentType_##name(CComponentManager&); \
m_CurrentComponent = CID_##name; \
RegisterComponentType_##name(*this);
#include "simulation2/TypeList.h"
m_CurrentComponent = CID__Invalid;
#undef MESSAGE
#undef INTERFACE
#undef COMPONENT
}
bool CComponentManager::LoadScript(const std::wstring& filename, bool hotload)
{
m_CurrentlyHotloading = hotload;
CVFSFile file;
PSRETURN loadOk = file.Load(g_VFS, filename);
debug_assert(loadOk == PSRETURN_OK); // TODO
std::wstring content(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize()); // TODO: encodings etc
bool ok = m_ScriptInterface.LoadScript(filename, content);
return ok;
}
void CComponentManager::Script_RegisterComponentType(void* cbdata, int iid, std::string cname, CScriptVal ctor)
{
CComponentManager* componentManager = static_cast (cbdata);
// Find the C++ component that wraps the interface
int cidWrapper = componentManager->GetScriptWrapper(iid);
if (cidWrapper == CID__Invalid)
{
componentManager->m_ScriptInterface.ReportError("Invalid interface id");
return;
}
const ComponentType& ctWrapper = componentManager->m_ComponentTypesById[cidWrapper];
bool mustReloadComponents = false; // for hotloading
ComponentTypeId cid = componentManager->LookupCID(cname);
if (cid == CID__Invalid)
{
// Allocate a new cid number
cid = componentManager->m_NextScriptComponentTypeId++;
componentManager->m_ComponentTypeIdsByName[cname] = cid;
}
else
{
// Component type is already loaded, so do hotloading:
if (!componentManager->m_CurrentlyHotloading)
{
componentManager->m_ScriptInterface.ReportError("Registering component type with already-registered name"); // TODO: report the actual name
return;
}
const ComponentType& ctPrevious = componentManager->m_ComponentTypesById[cid];
// We can only replace scripted component types, not native ones
if (ctPrevious.type != CT_Script)
{
componentManager->m_ScriptInterface.ReportError("Hotloading script component type with same name as native component");
return;
}
// We don't support changing the IID of a component type (it would require fiddling
// around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity)
if (ctPrevious.iid != iid)
{
// ...though it only matters if any components exist with this type
if (!componentManager->m_ComponentsByTypeId[cid].empty())
{
componentManager->m_ScriptInterface.ReportError("Hotloading script component type mustn't change interface ID");
return;
}
}
// Clean up the old component type
componentManager->m_ScriptInterface.RemoveRoot(&componentManager->m_ComponentTypesById[cid].ctor);
// Remove its old message subscriptions
std::map >::iterator it;
for (it = componentManager->m_LocalMessageSubscriptions.begin(); it != componentManager->m_LocalMessageSubscriptions.end(); ++it)
{
std::vector& types = it->second;
std::vector::iterator ctit = find(types.begin(), types.end(), cid);
if (ctit != types.end())
types.erase(ctit);
}
for (it = componentManager->m_GlobalMessageSubscriptions.begin(); it != componentManager->m_GlobalMessageSubscriptions.end(); ++it)
{
std::vector& types = it->second;
std::vector::iterator ctit = find(types.begin(), types.end(), cid);
if (ctit != types.end())
types.erase(ctit);
}
mustReloadComponents = true;
}
std::string schema = " ";
{
CScriptValRooted prototype;
if (componentManager->m_ScriptInterface.GetProperty(ctor.get(), "prototype", prototype) &&
componentManager->m_ScriptInterface.HasProperty(prototype.get(), "Schema"))
{
componentManager->m_ScriptInterface.GetProperty(prototype.get(), "Schema", schema);
}
}
// Construct a new ComponentType, using the wrapper's alloc functions
ComponentType ct = { CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, ctor.get() };
componentManager->m_ComponentTypesById[cid] = ct;
componentManager->m_CurrentComponent = cid; // needed by Subscribe
// Stop the ctor getting GCed
componentManager->m_ScriptInterface.AddRoot(&componentManager->m_ComponentTypesById[cid].ctor, "ComponentType ctor");
// TODO: check carefully that roots will never get leaked etc
// Find all the ctor prototype's On* methods, and subscribe to the appropriate messages:
CScriptVal proto;
if (!componentManager->m_ScriptInterface.GetProperty(ctor.get(), "prototype", proto))
return; // error
std::vector methods;
if (!componentManager->m_ScriptInterface.EnumeratePropertyNamesWithPrefix(proto.get(), "On", methods))
return; // error
for (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it)
{
std::string name = (*it).substr(2); // strip the "On" prefix
// Handle "OnGlobalFoo" functions specially
bool isGlobal = false;
if (name.substr(0, 6) == "Global")
{
isGlobal = true;
name = name.substr(6);
}
std::map::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name);
if (mit == componentManager->m_MessageTypeIdsByName.end())
{
std::string msg = "Registered component has unrecognised '" + *it + "' message handler method";
componentManager->m_ScriptInterface.ReportError(msg.c_str());
return;
}
if (isGlobal)
componentManager->SubscribeGloballyToMessageType(mit->second);
else
componentManager->SubscribeToMessageType(mit->second);
}
componentManager->m_CurrentComponent = CID__Invalid;
if (mustReloadComponents)
{
// For every script component with this cid, we need to switch its
// prototype from the old constructor's prototype property to the new one's
const std::map& comps = componentManager->m_ComponentsByTypeId[cid];
std::map::const_iterator eit = comps.begin();
for (; eit != comps.end(); ++eit)
{
jsval instance = eit->second->GetJSInstance();
if (instance)
componentManager->m_ScriptInterface.SetPrototype(instance, proto.get());
}
}
}
void CComponentManager::Script_RegisterInterface(void* cbdata, std::string name)
{
CComponentManager* componentManager = static_cast (cbdata);
std::map::iterator it = componentManager->m_InterfaceIdsByName.find(name);
if (it != componentManager->m_InterfaceIdsByName.end())
{
// Redefinitions are fine (and just get ignored) when hotloading; otherwise
// they're probably unintentional and should be reported
if (!componentManager->m_CurrentlyHotloading)
componentManager->m_ScriptInterface.ReportError("Registering interface with already-registered name"); // TODO: report the actual name
return;
}
// IIDs start at 1, so size+1 is the next unused one
size_t id = componentManager->m_InterfaceIdsByName.size() + 1;
componentManager->m_InterfaceIdsByName[name] = id;
- componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int )id);
+ componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id);
+}
+
+void CComponentManager::Script_RegisterMessageType(void* cbdata, std::string name)
+{
+ CComponentManager* componentManager = static_cast (cbdata);
+
+ std::map::iterator it = componentManager->m_MessageTypeIdsByName.find(name);
+ if (it != componentManager->m_MessageTypeIdsByName.end())
+ {
+ // Redefinitions are fine (and just get ignored) when hotloading; otherwise
+ // they're probably unintentional and should be reported
+ if (!componentManager->m_CurrentlyHotloading)
+ componentManager->m_ScriptInterface.ReportError("Registering message type with already-registered name"); // TODO: report the actual name
+ return;
+ }
+
+ // MTIDs start at 1, so size+1 is the next unused one
+ size_t id = componentManager->m_MessageTypeIdsByName.size() + 1;
+ componentManager->RegisterMessageType(id, name.c_str());
+ componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id);
}
void CComponentManager::Script_RegisterGlobal(void* cbdata, std::string name, CScriptVal value)
{
CComponentManager* componentManager = static_cast (cbdata);
// Set the value, and accept duplicates only if hotloading (otherwise it's an error,
// in order to detect accidental duplicate definitions of globals)
componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading);
}
IComponent* CComponentManager::Script_QueryInterface(void* cbdata, int ent, int iid)
{
CComponentManager* componentManager = static_cast (cbdata);
IComponent* component = componentManager->QueryInterface((entity_id_t)ent, iid);
return component;
}
+CMessage* CComponentManager::ConstructMessage(int mtid, CScriptVal data)
+{
+ if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here)
+ LOGERROR(L"PostMessage with invalid message type ID '%d'", mtid);
+
+ if (mtid < MT__LastNative)
+ {
+ return CMessageFromJSVal(mtid, m_ScriptInterface, data.get());
+ }
+ else
+ {
+ return new CMessageScripted(mtid, m_MessageTypeNamesById[mtid],
+ CScriptValRooted(m_ScriptInterface.GetContext(), data));
+ }
+}
+
void CComponentManager::Script_PostMessage(void* cbdata, int ent, int mtid, CScriptVal data)
{
CComponentManager* componentManager = static_cast (cbdata);
- CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
+
+ CMessage* msg = componentManager->ConstructMessage(mtid, data);
if (!msg)
return; // error
componentManager->PostMessage(ent, *msg);
delete msg;
}
void CComponentManager::Script_BroadcastMessage(void* cbdata, int mtid, CScriptVal data)
{
CComponentManager* componentManager = static_cast (cbdata);
- CMessage* msg = CMessageFromJSVal(mtid, componentManager->m_ScriptInterface, data.get());
+
+ CMessage* msg = componentManager->ConstructMessage(mtid, data);
if (!msg)
return; // error
componentManager->BroadcastMessage(*msg);
delete msg;
}
int CComponentManager::Script_AddEntity(void* cbdata, std::string templateName)
{
CComponentManager* componentManager = static_cast (cbdata);
std::wstring name(templateName.begin(), templateName.end());
// TODO: should validate the string to make sure it doesn't contain scary characters
// that will let it access non-component-template files
entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewEntity());
return (int)ent;
}
int CComponentManager::Script_AddLocalEntity(void* cbdata, std::string templateName)
{
CComponentManager* componentManager = static_cast (cbdata);
std::wstring name(templateName.begin(), templateName.end());
// TODO: should validate the string to make sure it doesn't contain scary characters
// that will let it access non-component-template files
entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewLocalEntity());
return (int)ent;
}
void CComponentManager::Script_DestroyEntity(void* cbdata, int ent)
{
CComponentManager* componentManager = static_cast (cbdata);
componentManager->DestroyComponentsSoon(ent);
}
void CComponentManager::ResetState()
{
// Delete all IComponents
std::map >::iterator iit = m_ComponentsByTypeId.begin();
for (; iit != m_ComponentsByTypeId.end(); ++iit)
{
std::map::iterator eit = iit->second.begin();
for (; eit != iit->second.end(); ++eit)
{
eit->second->Deinit(m_SimContext);
m_ComponentTypesById[iit->first].dealloc(eit->second);
}
}
m_ComponentsByInterface.clear();
m_ComponentsByTypeId.clear();
m_DestructionQueue.clear();
// Reset IDs
m_NextEntityId = SYSTEM_ENTITY + 1;
m_NextLocalEntityId = FIRST_LOCAL_ENTITY;
}
void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc,
const char* name, const std::string& schema)
{
ComponentType c = { CT_Native, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
}
void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc,
DeallocFunc dealloc, const char* name, const std::string& schema)
{
ComponentType c = { CT_ScriptWrapper, iid, alloc, dealloc, name, schema, 0 };
m_ComponentTypesById.insert(std::make_pair(cid, c));
m_ComponentTypeIdsByName[name] = cid;
// TODO: merge with RegisterComponentType
}
void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name)
{
m_MessageTypeIdsByName[name] = mtid;
+ m_MessageTypeNamesById[mtid] = name;
}
void CComponentManager::SubscribeToMessageType(MessageTypeId mtid)
{
// TODO: verify mtid
debug_assert(m_CurrentComponent != CID__Invalid);
std::vector& types = m_LocalMessageSubscriptions[mtid];
types.push_back(m_CurrentComponent);
std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents
}
void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid)
{
// TODO: verify mtid
debug_assert(m_CurrentComponent != CID__Invalid);
std::vector& types = m_GlobalMessageSubscriptions[mtid];
types.push_back(m_CurrentComponent);
std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents
}
CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const
{
std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname);
if (it == m_ComponentTypeIdsByName.end())
return CID__Invalid;
return it->second;
}
std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const
{
std::map::const_iterator it = m_ComponentTypesById.find(cid);
if (it == m_ComponentTypesById.end())
return "";
return it->second.name;
}
CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid)
{
if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1
return CID_UnknownScript;
std::map::const_iterator it = m_ComponentTypesById.begin();
for (; it != m_ComponentTypesById.end(); ++it)
if (it->second.iid == iid && it->second.type == CT_ScriptWrapper)
return it->first;
LOGERROR(L"No script wrapper found for interface id %d", iid); // TODO: report name (if iid is valid at all)
return CID__Invalid;
}
entity_id_t CComponentManager::AllocateNewEntity()
{
entity_id_t id = m_NextEntityId++;
// TODO: check for overflow
return id;
}
entity_id_t CComponentManager::AllocateNewLocalEntity()
{
entity_id_t id = m_NextLocalEntityId++;
// TODO: check for overflow
return id;
}
entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId)
{
// TODO: ensure this ID hasn't been allocated before
// (this might occur with broken map files)
entity_id_t id = preferredId;
// Ensure this ID won't be allocated again
if (id >= m_NextEntityId)
m_NextEntityId = id+1;
// TODO: check for overflow
return id;
}
bool CComponentManager::AddComponent(entity_id_t ent, ComponentTypeId cid, const CParamNode& paramNode)
{
IComponent* component = ConstructComponent(ent, cid);
if (!component)
return false;
component->Init(m_SimContext, paramNode);
return true;
}
IComponent* CComponentManager::ConstructComponent(entity_id_t ent, ComponentTypeId cid)
{
std::map::const_iterator it = m_ComponentTypesById.find(cid);
if (it == m_ComponentTypesById.end())
{
LOGERROR(L"Invalid component id %d", cid);
return NULL;
}
const ComponentType& ct = it->second;
std::map& emap1 = m_ComponentsByInterface[ct.iid];
if (emap1.find(ent) != emap1.end())
{
LOGERROR(L"Multiple components for interface %d", ct.iid);
return NULL;
}
std::map& emap2 = m_ComponentsByTypeId[cid];
// If this is a scripted component, construct the appropriate JS object first
jsval obj = 0;
if (ct.type == CT_Script)
{
obj = m_ScriptInterface.CallConstructor(ct.ctor);
if (!obj)
{
LOGERROR(L"Script component constructor failed");
return NULL;
}
}
// Construct the new component
IComponent* component = ct.alloc(m_ScriptInterface, obj);
debug_assert(component);
component->SetEntityId(ent);
component->SetSimContext(m_SimContext);
// Store a reference to the new component
emap1.insert(std::make_pair(ent, component));
emap2.insert(std::make_pair(ent, component));
// TODO: We need to more careful about this - if an entity is constructed by a component
// while we're iterating over all components, this will invalidate the iterators and everything
// will break.
// We probably need some kind of delayed addition, so they get pushed onto a queue and then
// inserted into the world later on. (Be careful about immediation deletion in that case, too.)
return component;
}
void CComponentManager::AddMockComponent(entity_id_t ent, InterfaceId iid, IComponent& component)
{
// Just add it into the by-interface map, not the by-component-type map,
// so it won't be considered for messages or deletion etc
std::map& emap1 = m_ComponentsByInterface[iid];
if (emap1.find(ent) != emap1.end())
debug_warn(L"Multiple components for interface");
emap1.insert(std::make_pair(ent, &component));
}
entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent)
{
ICmpTemplateManager *tempMan = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager));
if (!tempMan)
{
debug_warn(L"No ICmpTemplateManager loaded");
return INVALID_ENTITY;
}
// TODO: should assert that ent doesn't exist
const CParamNode* tmpl = tempMan->LoadTemplate(ent, CStr8(templateName), -1);
if (!tmpl)
return INVALID_ENTITY; // LoadTemplate will have reported the error
// Construct a component for each child of the root element
const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren();
for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it)
{
// Ignore attributes on the root element
if (it->first.length() && it->first[0] == '@')
continue;
CComponentManager::ComponentTypeId cid = LookupCID(it->first);
if (cid == CID__Invalid)
{
LOGERROR(L"Unrecognised component type name '%hs' in entity template '%ls'", it->first.c_str(), templateName.c_str());
return INVALID_ENTITY;
}
if (!AddComponent(ent, cid, it->second))
{
LOGERROR(L"Failed to construct component type name '%hs' in entity template '%ls'", it->first.c_str(), templateName.c_str());
return INVALID_ENTITY;
}
// TODO: maybe we should delete already-constructed components if one of them fails?
}
+ CMessageCreate msg(ent);
+ PostMessage(ent, msg);
+
return ent;
}
void CComponentManager::DestroyComponentsSoon(entity_id_t ent)
{
m_DestructionQueue.push_back(ent);
}
void CComponentManager::FlushDestroyedComponents()
{
// Make a copy of the destruction queue, so that the iterators won't be invalidated if the
// CMessageDestroy handlers try to destroy more entities themselves
std::vector queue;
queue.swap(m_DestructionQueue);
for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it)
{
entity_id_t ent = *it;
CMessageDestroy msg(ent);
PostMessage(ent, msg);
// Destroy the components, and remove from m_ComponentsByTypeId:
std::map >::iterator iit = m_ComponentsByTypeId.begin();
for (; iit != m_ComponentsByTypeId.end(); ++iit)
{
std::map::iterator eit = iit->second.find(ent);
if (eit != iit->second.end())
{
eit->second->Deinit(m_SimContext);
m_ComponentTypesById[iit->first].dealloc(eit->second);
iit->second.erase(ent);
}
}
// Remove from m_ComponentsByInterface
std::map >::iterator ifcit = m_ComponentsByInterface.begin();
for (; ifcit != m_ComponentsByInterface.end(); ++ifcit)
{
ifcit->second.erase(ent);
}
}
}
IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const
{
std::map >::const_iterator iit = m_ComponentsByInterface.find(iid);
if (iit == m_ComponentsByInterface.end())
{
// Invalid iid, or no entities implement this interface
return NULL;
}
std::map::const_iterator eit = iit->second.find(ent);
if (eit == iit->second.end())
{
// This entity doesn't implement this interface
return NULL;
}
return eit->second;
}
static std::map g_EmptyEntityMap;
const std::map& CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const
{
std::map >::const_iterator iit = m_ComponentsByInterface.find(iid);
if (iit == m_ComponentsByInterface.end())
{
// Invalid iid, or no entities implement this interface
return g_EmptyEntityMap;
}
return iit->second;
}
void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) const
{
// Send the message to components of ent, that subscribed locally to this message
std::map >::const_iterator it;
it = m_LocalMessageSubscriptions.find(msg.GetType());
if (it != m_LocalMessageSubscriptions.end())
{
std::vector::const_iterator ctit = it->second.begin();
for (; ctit != it->second.end(); ++ctit)
{
std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit);
if (emap == m_ComponentsByTypeId.end())
continue;
std::map::const_iterator eit = emap->second.find(ent);
if (eit != emap->second.end())
eit->second->HandleMessage(m_SimContext, msg, false);
}
}
SendGlobalMessage(msg);
}
void CComponentManager::BroadcastMessage(const CMessage& msg) const
{
// Send the message to components of all entities that subscribed locally to this message
std::map >::const_iterator it;
it = m_LocalMessageSubscriptions.find(msg.GetType());
if (it != m_LocalMessageSubscriptions.end())
{
std::vector::const_iterator ctit = it->second.begin();
for (; ctit != it->second.end(); ++ctit)
{
std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit);
if (emap == m_ComponentsByTypeId.end())
continue;
std::map::const_iterator eit = emap->second.begin();
for (; eit != emap->second.end(); ++eit)
eit->second->HandleMessage(m_SimContext, msg, false);
}
}
SendGlobalMessage(msg);
}
void CComponentManager::SendGlobalMessage(const CMessage& msg) const
{
// (Common functionality for PostMessage and BroadcastMessage)
// Send the message to components of all entities that subscribed globally to this message
std::map >::const_iterator it;
it = m_GlobalMessageSubscriptions.find(msg.GetType());
if (it != m_GlobalMessageSubscriptions.end())
{
std::vector::const_iterator ctit = it->second.begin();
for (; ctit != it->second.end(); ++ctit)
{
std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit);
if (emap == m_ComponentsByTypeId.end())
continue;
std::map::const_iterator eit = emap->second.begin();
for (; eit != emap->second.end(); ++eit)
eit->second->HandleMessage(m_SimContext, msg, true);
}
}
}
std::string CComponentManager::GenerateSchema()
{
std::string schema =
""
""
" 0 "
" "
""
" 0 "
" "
""
""
""
" "
" "
""
" "
""
" "
" "
" "
" ";
std::map > interfaceComponentTypes;
std::vector componentTypes;
for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it)
{
schema +=
""
""
"" + it->second.schema + " "
" "
" ";
interfaceComponentTypes[it->second.iid].push_back(it->second.name);
componentTypes.push_back(it->second.name);
}
// Declare the implementation of each interface, for documentation
for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it)
{
schema += "";
std::vector& cts = interfaceComponentTypes[it->second];
for (size_t i = 0; i < cts.size(); ++i)
schema += "";
schema += " ";
}
// List all the component types, in alphabetical order (to match the reordering performed by CParamNode).
// (We do it this way, rather than ing all the interface definitions (which would additionally perform
// a check that we don't use multiple component types of the same interface in one file), because libxml2 gives
// useless error messages in the latter case; this way lets it report the real error.)
std::sort(componentTypes.begin(), componentTypes.end());
schema +=
""
""
" ";
for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it)
schema += " ";
schema +=
" "
" ";
schema += " ";
// TODO: pretty-print
return schema;
}
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 7762)
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.cpp (revision 7763)
@@ -1,30 +1,32 @@
/* Copyright (C) 2010 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 .
*/
#include "precompiled.h"
#include "ICmpUnitMotion.h"
#include "simulation2/system/InterfaceScripted.h"
BEGIN_INTERFACE_WRAPPER(UnitMotion)
DEFINE_INTERFACE_METHOD_2("MoveToPoint", bool, ICmpUnitMotion, MoveToPoint, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("IsInAttackRange", bool, ICmpUnitMotion, IsInAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
DEFINE_INTERFACE_METHOD_3("MoveToAttackRange", bool, ICmpUnitMotion, MoveToAttackRange, entity_id_t, entity_pos_t, entity_pos_t)
-DEFINE_INTERFACE_METHOD_0("GetSpeed", fixed, ICmpUnitMotion, GetSpeed)
+DEFINE_INTERFACE_METHOD_4("MoveToPointRange", bool, ICmpUnitMotion, MoveToPointRange, entity_pos_t, entity_pos_t, entity_pos_t, entity_pos_t)
+DEFINE_INTERFACE_METHOD_0("StopMoving", void, ICmpUnitMotion, StopMoving)
+DEFINE_INTERFACE_METHOD_1("SetSpeedFactor", void, ICmpUnitMotion, SetSpeedFactor, fixed)
DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpUnitMotion, SetDebugOverlay, bool)
END_INTERFACE_WRAPPER(UnitMotion)
Index: ps/trunk/source/simulation2/components/ICmpUnitMotion.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 7762)
+++ ps/trunk/source/simulation2/components/ICmpUnitMotion.h (revision 7763)
@@ -1,81 +1,96 @@
/* Copyright (C) 2010 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 .
*/
#ifndef INCLUDED_ICMPUNITMOTION
#define INCLUDED_ICMPUNITMOTION
#include "simulation2/system/Interface.h"
#include "ICmpPosition.h" // for entity_pos_t
/**
* Motion interface for entities with complex movement capabilities.
* (Simpler motion is handled by ICmpMotion instead.)
*
* Currently this is limited to telling the entity to walk to a point.
* Eventually it should support different movement speeds, moving to areas
* instead of points, moving as part of a group, moving as part of a formation,
* etc.
*/
class ICmpUnitMotion : public IComponent
{
public:
/**
* Attempt to walk to a given point, or as close as possible.
* If the unit cannot move anywhere at all, or if there is some other error, then
* returns false.
* Otherwise, sends a MotionChanged message and returns true; it will send another
* MotionChanged message (with speed 0) once it has reached the target or otherwise
* given up trying to reach it.
*/
virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z) = 0;
/**
* Determine whether the target is within the given range, using the same measurement
* as MoveToAttackRange.
*/
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
- * Attempt to walk into range of a given target, or as close as possible.
+ * Attempt to walk into range of a given target entity, or as close as possible.
* If the unit is already in range, or cannot move anywhere at all, or if there is
* some other error, then returns false.
* Otherwise, sends a MotionChanged message and returns true; it will send another
* MotionChanged message (with speed 0) once it has reached the target range (such that
* IsInAttackRange should return true) or otherwise given up trying to reach it.
*/
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) = 0;
/**
+ * See MoveToAttackRange, but the target is the given point.
+ */
+ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) = 0;
+
+ /**
+ * Stop moving immediately.
+ */
+ virtual void StopMoving() = 0;
+
+ /**
+ * Set the current movement speed to be the default multiplied by the given factor.
+ */
+ virtual void SetSpeedFactor(fixed factor) = 0;
+
+ /**
* Get the default speed that this unit will have when walking, in metres per second.
*/
- virtual fixed GetSpeed() = 0;
+ virtual fixed GetWalkSpeed() = 0;
/**
* Get the default speed that this unit will have when running, in metres per second.
*/
virtual fixed GetRunSpeed() = 0;
/**
* Toggle the rendering of debug info.
*/
virtual void SetDebugOverlay(bool enabled) = 0;
DECLARE_INTERFACE_TYPE(UnitMotion)
};
#endif // INCLUDED_ICMPUNITMOTION
Index: ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 7762)
+++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 7763)
@@ -1,941 +1,975 @@
/* Copyright (C) 2010 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 .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpUnitMotion.h"
#include "ICmpObstruction.h"
#include "ICmpObstructionManager.h"
#include "ICmpPosition.h"
#include "ICmpPathfinder.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/helpers/Geometry.h"
#include "simulation2/helpers/Render.h"
#include "graphics/Overlay.h"
#include "graphics/Terrain.h"
#include "maths/FixedVector2D.h"
#include "ps/Profile.h"
#include "renderer/Scene.h"
static const entity_pos_t WAYPOINT_ADVANCE_MIN = entity_pos_t::FromInt(CELL_SIZE*4);
static const entity_pos_t WAYPOINT_ADVANCE_MAX = entity_pos_t::FromInt(CELL_SIZE*8);
static const entity_pos_t SHORT_PATH_SEARCH_RANGE = entity_pos_t::FromInt(CELL_SIZE*12);
static const CColor OVERLAY_COLOUR_PATH(1, 1, 1, 1);
static const CColor OVERLAY_COLOUR_PATH_ACTIVE(1, 1, 0, 1);
static const CColor OVERLAY_COLOUR_SHORT_PATH(1, 0, 0, 1);
class CCmpUnitMotion : public ICmpUnitMotion
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays
}
DEFAULT_COMPONENT_ALLOCATOR(UnitMotion)
bool m_DebugOverlayEnabled;
std::vector m_DebugOverlayLines;
std::vector m_DebugOverlayShortPathLines;
// Template state:
- fixed m_Speed; // in metres per second
+ fixed m_WalkSpeed; // in metres per second
fixed m_RunSpeed;
entity_pos_t m_Radius;
u8 m_PassClass;
u8 m_CostClass;
// Dynamic state:
+ fixed m_Speed;
bool m_HasTarget; // whether we currently have valid paths and targets
// These values contain undefined junk if !HasTarget:
ICmpPathfinder::Path m_Path;
ICmpPathfinder::Path m_ShortPath;
entity_pos_t m_ShortTargetX, m_ShortTargetZ;
ICmpPathfinder::Goal m_FinalGoal;
enum
{
IDLE,
WALKING,
STOPPING
};
int m_State;
static std::string GetSchema()
{
return
"Provides the unit with the ability to move around the world by itself. "
""
"7.0 "
"default "
"infantry "
" "
""
""
" "
""
""
""
" "
" "
" "
" "
" "
" "
" "
" "
""
" "
" "
""
" "
" ";
}
/*
* TODO: the running/charging thing needs to be designed and implemented
*/
virtual void Init(const CSimContext& context, const CParamNode& paramNode)
{
m_HasTarget = false;
- m_Speed = paramNode.GetChild("WalkSpeed").ToFixed();
+ m_WalkSpeed = paramNode.GetChild("WalkSpeed").ToFixed();
+ m_Speed = m_WalkSpeed;
if (paramNode.GetChild("Run").IsOk())
{
m_RunSpeed = paramNode.GetChild("Run").GetChild("Speed").ToFixed();
}
else
{
- m_RunSpeed = m_Speed;
+ m_RunSpeed = m_WalkSpeed;
}
CmpPtr cmpObstruction(context, GetEntityId());
if (!cmpObstruction.null())
m_Radius = cmpObstruction->GetUnitRadius();
CmpPtr cmpPathfinder(context, SYSTEM_ENTITY);
if (!cmpPathfinder.null())
{
m_PassClass = cmpPathfinder->GetPassabilityClass(paramNode.GetChild("PassabilityClass").ToASCIIString());
m_CostClass = cmpPathfinder->GetCostClass(paramNode.GetChild("CostClass").ToASCIIString());
}
m_State = IDLE;
m_DebugOverlayEnabled = false;
}
virtual void Deinit(const CSimContext& UNUSED(context))
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.Bool("has target", m_HasTarget);
if (m_HasTarget)
{
// TODO: m_Path
// TODO: m_FinalTargetAngle
}
// TODO: m_State
}
virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(context, paramNode);
deserialize.Bool(m_HasTarget);
if (m_HasTarget)
{
}
}
virtual void HandleMessage(const CSimContext& context, const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_Update:
{
fixed dt = static_cast (msg).turnLength;
if (m_State == STOPPING)
{
m_State = IDLE;
CMessageMotionChanged msg(fixed::Zero());
context.GetComponentManager().PostMessage(GetEntityId(), msg);
}
Move(dt);
break;
}
case MT_RenderSubmit:
{
const CMessageRenderSubmit& msgData = static_cast (msg);
RenderSubmit(msgData.collector);
break;
}
}
}
- virtual fixed GetSpeed()
+ virtual fixed GetWalkSpeed()
{
- return m_Speed;
+ return m_WalkSpeed;
}
virtual fixed GetRunSpeed()
{
return m_RunSpeed;
}
+ virtual void SetSpeedFactor(fixed factor)
+ {
+ m_Speed = m_WalkSpeed.Multiply(factor);
+ }
+
virtual void SetDebugOverlay(bool enabled)
{
m_DebugOverlayEnabled = enabled;
if (enabled)
{
RenderPath(m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
}
}
virtual bool MoveToPoint(entity_pos_t x, entity_pos_t z);
virtual bool MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
virtual bool IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange);
+ virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange);
+
+ virtual void StopMoving()
+ {
+ SwitchState(IDLE);
+ }
private:
/**
* Check whether moving from pos to target is safe (won't hit anything).
* If safe, returns true (the caller should do cmpPosition->MoveTo).
* Otherwise returns false, and either computes a new path to use on the
* next turn or makes the unit stop.
*/
bool CheckMovement(CFixedVector2D pos, CFixedVector2D target);
/**
* Do the per-turn movement and other updates
*/
void Move(fixed dt);
void StopAndFaceGoal(CFixedVector2D pos);
/**
* Rotate to face towards the target point, given the current pos
*/
void FaceTowardsPoint(CFixedVector2D pos, entity_pos_t x, entity_pos_t z);
/**
* Change between idle/walking states; automatically sends MotionChanged messages when appropriate
*/
void SwitchState(int state);
bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius);
/**
* Recompute the whole path to the current goal.
* Returns false on error or if the unit can't move anywhere at all.
*/
bool RegeneratePath(CFixedVector2D pos, bool avoidMovingUnits);
/**
* Maybe select a new long waypoint if we're getting too close to the
* current one.
*/
void MaybePickNextWaypoint(const CFixedVector2D& pos);
/**
* Select a next long waypoint, given the current unit position.
* Also recomputes the short path to use that waypoint.
* Returns false on error, or if there is no waypoint to pick.
*/
bool PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits);
/**
* Select a new short waypoint as the current target,
* which possibly involves first selecting a new long waypoint.
* Returns false on error, or if there is no waypoint to pick.
*/
bool PickNextShortWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits);
/**
* Convert a path into a renderable list of lines
*/
void RenderPath(const ICmpPathfinder::Path& path, std::vector& lines, CColor color);
void RenderSubmit(SceneCollector& collector);
};
REGISTER_COMPONENT_TYPE(UnitMotion)
bool CCmpUnitMotion::CheckMovement(CFixedVector2D pos, CFixedVector2D target)
{
CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
NullObstructionFilter filter;
if (cmpObstructionManager->TestLine(filter, pos.X, pos.Y, target.X, target.Y, m_Radius))
{
// Oops, hit something
// TODO: we ought to wait for obstructions to move away instead of immediately throwing away the whole path
// TODO: actually a whole proper collision resolution thing needs to be designed and written
if (!RegeneratePath(pos, true))
{
// Oh dear, we can't find the path any more; give up
StopAndFaceGoal(pos);
return false;
}
// NOTE: it's theoretically possible that we will generate a waypoint we can reach without
// colliding with anything, but multiplying the movement vector by the timestep will result
// in a line that does collide (given numerical inaccuracies), so we'll get stuck in a loop
// of generating a new path and colliding whenever we try to follow it, and the unit will
// move nowhere.
// Hopefully this isn't common.
// Wait for the next Update before we try moving again
return false;
}
// NOTE: we ignore terrain here - we assume the pathfinder won't give us a path that crosses impassable
// terrain (which is a valid assumption) and that the terrain will never change (which is not).
// Probably not worth fixing since it'll happen very rarely.
return true;
}
void CCmpUnitMotion::Move(fixed dt)
{
PROFILE("Move");
if (!m_HasTarget)
return;
CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return;
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null())
return;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// We want to move (at most) m_Speed*dt units from pos towards the next waypoint
while (dt > fixed::Zero())
{
CFixedVector2D target(m_ShortTargetX, m_ShortTargetZ);
CFixedVector2D offset = target - pos;
// Face towards the target
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
cmpPosition->TurnTo(angle);
// Find the speed factor of the underlying terrain
// (We only care about the tile we start on - it doesn't matter if we're moving
// partially onto a much slower/faster tile)
fixed terrainSpeed = cmpPathfinder->GetMovementSpeed(pos.X, pos.Y, m_CostClass);
// Work out how far we can travel in dt
fixed maxdist = m_Speed.Multiply(terrainSpeed).Multiply(dt);
// If the target is close, we can move there directly
fixed offsetLength = offset.Length();
if (offsetLength <= maxdist)
{
if (!CheckMovement(pos, target))
return;
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
// Spend the rest of the time heading towards the next waypoint
dt = dt - (offset.Length() / m_Speed);
MaybePickNextWaypoint(pos);
if (PickNextShortWaypoint(pos, false))
continue;
// We ran out of usable waypoints, so stop now
StopAndFaceGoal(pos);
return;
}
else
{
// Not close enough, so just move in the right direction
offset.Normalize(maxdist);
target = pos + offset;
if (!CheckMovement(pos, target))
return;
pos = target;
cmpPosition->MoveTo(pos.X, pos.Y);
MaybePickNextWaypoint(pos);
return;
}
}
}
void CCmpUnitMotion::StopAndFaceGoal(CFixedVector2D pos)
{
SwitchState(IDLE);
FaceTowardsPoint(pos, m_FinalGoal.x, m_FinalGoal.z);
// TODO: if the goal was a square building, we ought to point towards the
// nearest point on the square, not towards its center
}
void CCmpUnitMotion::FaceTowardsPoint(CFixedVector2D pos, entity_pos_t x, entity_pos_t z)
{
CFixedVector2D target(x, z);
CFixedVector2D offset = target - pos;
if (!offset.IsZero())
{
entity_angle_t angle = atan2_approx(offset.X, offset.Y);
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null())
return;
cmpPosition->TurnTo(angle);
}
}
void CCmpUnitMotion::SwitchState(int state)
{
debug_assert(state == IDLE || state == WALKING);
if (state == IDLE)
m_HasTarget = false;
// IDLE -> IDLE -- no change
// IDLE -> WALKING -- send a MotionChanged(speed) message
// WALKING -> IDLE -- set to STOPPING, so we'll send MotionChanged(0) in the next Update
// WALKING -> WALKING -- send a MotionChanged(speed) message
// STOPPING -> IDLE -- stay in STOPPING
// STOPPING -> WALKING -- set to WALKING, send MotionChanged(speed)
if (state == WALKING)
{
CMessageMotionChanged msg(m_Speed);
GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg);
}
if (m_State == IDLE && state == WALKING)
{
m_State = WALKING;
return;
}
if (m_State == WALKING && state == IDLE)
{
m_State = STOPPING;
return;
}
if (m_State == STOPPING && state == IDLE)
{
return;
}
if (m_State == STOPPING && state == WALKING)
{
m_State = WALKING;
return;
}
}
bool CCmpUnitMotion::MoveToPoint(entity_pos_t x, entity_pos_t z)
{
PROFILE("MoveToPoint");
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// Reset any current movement
m_HasTarget = false;
ICmpPathfinder::Goal goal;
CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::ObstructionSquare obstruction;
if (cmpObstructionManager->FindMostImportantObstruction(x, z, m_Radius, obstruction))
{
// If we're aiming inside a building, then aim for the outline of the building instead
// TODO: if we're aiming at a unit then maybe a circle would look nicer?
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.x = obstruction.x;
goal.z = obstruction.z;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = entity_pos_t::FromInt(1) / 4; // nudge the goal outwards so it doesn't intersect the building itself
goal.hw = obstruction.hw + m_Radius + delta;
goal.hh = obstruction.hh + m_Radius + delta;
}
else
{
// Unobstructed - head directly for the goal
goal.type = ICmpPathfinder::Goal::POINT;
goal.x = x;
goal.z = z;
}
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
return true;
}
bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t hw, entity_pos_t hh, entity_pos_t circleRadius)
{
// Given a square, plus a target range we should reach, the shape at that distance
// is a round-cornered square which we can approximate as either a circle or as a square.
// Choose the shape that will minimise the worst-case error:
// For a square, error is (sqrt(2)-1) * range at the corners
entity_pos_t errSquare = (entity_pos_t::FromInt(4142)/10000).Multiply(range);
// For a circle, error is radius-hw at the sides and radius-hh at the top/bottom
entity_pos_t errCircle = circleRadius - std::min(hw, hh);
return (errCircle < errSquare);
}
+static const entity_pos_t g_GoalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
+
bool CCmpUnitMotion::MoveToAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
PROFILE("MoveToAttackRange");
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
// Reset any current movement
m_HasTarget = false;
- ICmpPathfinder::Goal goal;
-
CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::tag_t tag;
CmpPtr cmpObstruction(GetSimContext(), target);
if (!cmpObstruction.null())
tag = cmpObstruction->GetObstruction();
/*
* If we're starting outside the maxRange, we need to move closer in.
* If we're starting inside the minRange, we need to move further out.
* These ranges are measured from the center of this entity to the edge of the target;
* we add the goal range onto the size of the target shape to get the goal shape.
* (Then we extend it outwards/inwards by a little bit to be sure we'll end up
* within the right range, in case of minor numerical inaccuracies.)
*
* There's a bit of a problem with large square targets:
* the pathfinder only lets us move to goals that are squares, but the points an equal
* distance from the target make a rounded square shape instead.
*
* When moving closer, we could shrink the goal radius to 1/sqrt(2) so the goal shape fits entirely
* within the desired rounded square, but that gives an unfair advantage to attackers who approach
* the target diagonally.
*
* If the target is small relative to the range (e.g. archers attacking anything),
* then we cheat and pretend the target is actually a circle.
* (TODO: that probably looks rubbish for things like walls?)
*
* If the target is large relative to the range (e.g. melee units attacking buildings),
* then we multiply maxRange by approx 1/sqrt(2) to guarantee they'll always aim close enough.
* (Those units should set minRange to 0 so they'll never be considered *too* close.)
*/
- const entity_pos_t goalDelta = entity_pos_t::FromInt(CELL_SIZE)/4; // for extending the goal outwards/inwards a little bit
-
if (tag.valid())
{
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
+ ICmpPathfinder::Goal goal;
goal.x = obstruction.x;
goal.z = obstruction.z;
entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize);
if (distance < minRange)
{
// Too close to the square - need to move away
- entity_pos_t goalDistance = minRange + goalDelta;
+ entity_pos_t goalDistance = minRange + g_GoalDelta;
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself
goal.hw = obstruction.hw + delta;
goal.hh = obstruction.hh + delta;
}
else if (distance < maxRange)
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
else
{
// We might need to move closer:
// Circumscribe the square
entity_pos_t circleRadius = halfSize.Length();
if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius))
{
// The target is small relative to our range, so pretend it's a circle
// Note that the distance to the circle will always be less than
// the distance to the square, so the previous "distance < maxRange"
// check is still valid (though not sufficient)
entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
if (circleDistance < maxRange)
{
// We're already in range - no need to move anywhere
FaceTowardsPoint(pos, goal.x, goal.z);
return false;
}
- entity_pos_t goalDistance = maxRange - goalDelta;
+ entity_pos_t goalDistance = maxRange - g_GoalDelta;
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.hw = circleRadius + goalDistance;
}
else
{
// The target is large relative to our range, so treat it as a square and
// get close enough that the diagonals come within range
- entity_pos_t goalDistance = (maxRange - goalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2)
+ entity_pos_t goalDistance = (maxRange - g_GoalDelta)*2 / 3; // multiply by slightly less than 1/sqrt(2)
goal.type = ICmpPathfinder::Goal::SQUARE;
goal.u = obstruction.u;
goal.v = obstruction.v;
entity_pos_t delta = std::max(goalDistance, m_Radius + entity_pos_t::FromInt(CELL_SIZE)/16); // ensure it's far enough to not intersect the building itself
goal.hw = obstruction.hw + delta;
goal.hh = obstruction.hh + delta;
}
}
+
+ m_FinalGoal = goal;
+ if (!RegeneratePath(pos, false))
+ return false;
+
+ SwitchState(WALKING);
+ return true;
}
else
{
// The target didn't have an obstruction or obstruction shape, so treat it as a point instead
CmpPtr cmpTargetPosition(GetSimContext(), target);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
return false;
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
- entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length();
+ return MoveToPointRange(targetPos.X, targetPos.Z, minRange, maxRange);
+ }
+}
- entity_pos_t goalDistance;
- if (distance < minRange)
- {
- goalDistance = minRange + goalDelta;
- }
- else if (distance > maxRange)
- {
- goalDistance = maxRange - goalDelta;
- }
- else
- {
- // We're already in range - no need to move anywhere
- FaceTowardsPoint(pos, goal.x, goal.z);
- return false;
- }
+bool CCmpUnitMotion::MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange)
+{
+ PROFILE("MoveToPointRange");
+
+ CmpPtr cmpPosition(GetSimContext(), GetEntityId());
+ if (cmpPosition.null() || !cmpPosition->IsInWorld())
+ return false;
- // TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target)
+ CFixedVector3D pos3 = cmpPosition->GetPosition();
+ CFixedVector2D pos (pos3.X, pos3.Z);
- goal.type = ICmpPathfinder::Goal::CIRCLE;
- goal.x = targetPos.X;
- goal.z = targetPos.Z;
- goal.hw = m_Radius + goalDistance;
+ entity_pos_t distance = (pos - CFixedVector2D(x, z)).Length();
+
+ entity_pos_t goalDistance;
+ if (distance < minRange)
+ {
+ goalDistance = minRange + g_GoalDelta;
+ }
+ else if (distance > maxRange)
+ {
+ goalDistance = maxRange - g_GoalDelta;
}
+ else
+ {
+ // We're already in range - no need to move anywhere
+ FaceTowardsPoint(pos, x, z);
+ return false;
+ }
+
+ // TODO: what happens if goalDistance < 0? (i.e. we probably can never get close enough to the target)
+
+ ICmpPathfinder::Goal goal;
+ goal.type = ICmpPathfinder::Goal::CIRCLE;
+ goal.x = x;
+ goal.z = z;
+ goal.hw = m_Radius + goalDistance;
m_FinalGoal = goal;
if (!RegeneratePath(pos, false))
return false;
SwitchState(WALKING);
return true;
}
bool CCmpUnitMotion::IsInAttackRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange)
{
// This function closely mirrors MoveToAttackRange - it needs to return true
// after that Move has completed
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (cmpPosition.null() || !cmpPosition->IsInWorld())
return false;
CFixedVector3D pos3 = cmpPosition->GetPosition();
CFixedVector2D pos (pos3.X, pos3.Z);
CmpPtr cmpObstructionManager(GetSimContext(), SYSTEM_ENTITY);
if (cmpObstructionManager.null())
return false;
ICmpObstructionManager::tag_t tag;
CmpPtr cmpObstruction(GetSimContext(), target);
if (!cmpObstruction.null())
tag = cmpObstruction->GetObstruction();
entity_pos_t distance;
if (tag.valid())
{
ICmpObstructionManager::ObstructionSquare obstruction = cmpObstructionManager->GetObstruction(tag);
CFixedVector2D halfSize(obstruction.hw, obstruction.hh);
entity_pos_t distance = Geometry::DistanceToSquare(pos - CFixedVector2D(obstruction.x, obstruction.z), obstruction.u, obstruction.v, halfSize);
// See if we're too close to the target square
if (distance < minRange)
return false;
// See if we're close enough to the target square
if (distance <= maxRange)
return true;
entity_pos_t circleRadius = halfSize.Length();
if (ShouldTreatTargetAsCircle(maxRange, obstruction.hw, obstruction.hh, circleRadius))
{
// The target is small relative to our range, so pretend it's a circle
// and see if we're close enough to that
entity_pos_t circleDistance = (pos - CFixedVector2D(obstruction.x, obstruction.z)).Length() - circleRadius;
if (circleDistance <= maxRange)
return true;
}
return false;
}
else
{
CmpPtr cmpTargetPosition(GetSimContext(), target);
if (cmpTargetPosition.null() || !cmpTargetPosition->IsInWorld())
return false;
CFixedVector3D targetPos = cmpTargetPosition->GetPosition();
entity_pos_t distance = (pos - CFixedVector2D(targetPos.X, targetPos.Z)).Length();
if (minRange <= distance && distance <= maxRange)
return true;
return false;
}
}
bool CCmpUnitMotion::RegeneratePath(CFixedVector2D pos, bool avoidMovingUnits)
{
CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return false;
m_Path.m_Waypoints.clear();
m_ShortPath.m_Waypoints.clear();
// TODO: if it's close then just do a short path, not a long path
cmpPathfinder->SetDebugPath(pos.X, pos.Y, m_FinalGoal, m_PassClass, m_CostClass);
cmpPathfinder->ComputePath(pos.X, pos.Y, m_FinalGoal, m_PassClass, m_CostClass, m_Path);
if (m_DebugOverlayEnabled)
RenderPath(m_Path, m_DebugOverlayLines, OVERLAY_COLOUR_PATH);
// If there's no waypoints then we've stopped already, otherwise move to the first one
if (m_Path.m_Waypoints.empty())
{
m_HasTarget = false;
return false;
}
else
{
return PickNextShortWaypoint(pos, avoidMovingUnits);
}
}
void CCmpUnitMotion::MaybePickNextWaypoint(const CFixedVector2D& pos)
{
if (m_Path.m_Waypoints.empty())
return;
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
if ((w - pos).Length() < WAYPOINT_ADVANCE_MIN)
PickNextWaypoint(pos, false); // TODO: handle failures?
}
bool CCmpUnitMotion::PickNextWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits)
{
if (m_Path.m_Waypoints.empty())
return false;
// First try to get the immediate next waypoint
entity_pos_t targetX = m_Path.m_Waypoints.back().x;
entity_pos_t targetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
// To smooth the motion and avoid grid-constrained movement and allow dynamic obstacle avoidance,
// try skipping some more waypoints if they're close enough
while (!m_Path.m_Waypoints.empty())
{
CFixedVector2D w(m_Path.m_Waypoints.back().x, m_Path.m_Waypoints.back().z);
if ((w - pos).Length() > WAYPOINT_ADVANCE_MAX)
break;
targetX = m_Path.m_Waypoints.back().x;
targetZ = m_Path.m_Waypoints.back().z;
m_Path.m_Waypoints.pop_back();
}
// Highlight the targeted waypoint
if (m_DebugOverlayEnabled)
m_DebugOverlayLines[m_Path.m_Waypoints.size()].m_Color = OVERLAY_COLOUR_PATH_ACTIVE;
// Now we need to recompute a short path to the waypoint
m_ShortPath.m_Waypoints.clear();
ICmpPathfinder::Goal goal;
if (m_Path.m_Waypoints.empty())
{
// This was the last waypoint - head for the exact goal
goal = m_FinalGoal;
}
else
{
// Head for somewhere near the waypoint (but allow some leeway in case it's obstructed)
goal.type = ICmpPathfinder::Goal::CIRCLE;
goal.hw = entity_pos_t::FromInt(CELL_SIZE*3/2);
goal.x = targetX;
goal.z = targetZ;
}
CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY);
if (cmpPathfinder.null())
return false;
// Set up the filter to avoid/ignore moving units
NullObstructionFilter filterNull;
StationaryObstructionFilter filterStationary;
const IObstructionTestFilter* filter;
if (avoidMovingUnits)
filter = &filterNull;
else
filter = &filterStationary;
cmpPathfinder->ComputeShortPath(*filter, pos.X, pos.Y, m_Radius, SHORT_PATH_SEARCH_RANGE, goal, m_PassClass, m_ShortPath);
if (m_DebugOverlayEnabled)
RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOUR_SHORT_PATH);
return true;
}
bool CCmpUnitMotion::PickNextShortWaypoint(const CFixedVector2D& pos, bool avoidMovingUnits)
{
// If we don't have a short path now
if (m_ShortPath.m_Waypoints.empty())
{
// Try to pick a new long waypoint (which will also recompute the short path)
if (!PickNextWaypoint(pos, avoidMovingUnits))
return false; // no waypoints left
if (m_ShortPath.m_Waypoints.empty())
return false; // we can't reach the next long waypoint or are already there
}
// Head towards the next short waypoint
m_ShortTargetX = m_ShortPath.m_Waypoints.back().x;
m_ShortTargetZ = m_ShortPath.m_Waypoints.back().z;
m_ShortPath.m_Waypoints.pop_back();
m_HasTarget = true;
return true;
}
void CCmpUnitMotion::RenderPath(const ICmpPathfinder::Path& path, std::vector& lines, CColor color)
{
bool floating = false;
CmpPtr cmpPosition(GetSimContext(), GetEntityId());
if (!cmpPosition.null())
floating = cmpPosition->IsFloating();
lines.clear();
std::vector waypointCoords;
for (size_t i = 0; i < path.m_Waypoints.size(); ++i)
{
float x = path.m_Waypoints[i].x.ToFloat();
float z = path.m_Waypoints[i].z.ToFloat();
waypointCoords.push_back(x);
waypointCoords.push_back(z);
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating);
}
lines.push_back(SOverlayLine());
lines.back().m_Color = color;
SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating);
}
void CCmpUnitMotion::RenderSubmit(SceneCollector& collector)
{
if (!m_DebugOverlayEnabled)
return;
for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i)
collector.Submit(&m_DebugOverlayLines[i]);
for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i)
collector.Submit(&m_DebugOverlayShortPathLines[i]);
}
Index: ps/trunk/binaries/data/mods/public/audio/actor/fauna/animal/chickens.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/audio/actor/fauna/animal/chickens.xml (revision 7762)
+++ ps/trunk/binaries/data/mods/public/audio/actor/fauna/animal/chickens.xml (revision 7763)
@@ -1,25 +1,25 @@
1
1
100
1
360
360
0
0
0
1
0.9
0
1.1
0.9
5
3
chicken_10.ogg
- /audio/actor/fauna/animal
+ audio/actor/fauna/animal
chicken_13.ogg
chicken_10.ogg
chicken_11.ogg
chicken_12.ogg
Index: ps/trunk/binaries/data/mods/public/gui/session_new/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session_new/session.xml (revision 7762)
+++ ps/trunk/binaries/data/mods/public/gui/session_new/session.xml (revision 7763)
@@ -1,466 +1,471 @@
onTick();
onSimulationUpdate();
this.hidden = !this.hidden;
-
+
+
+ this.hidden = !this.hidden;
+
+
Control all units
Display selection state
Pathfinder overlay
Engine.GuiInterfaceCall("SetPathfinderDebugOverlay", this.checked);
Obstruction overlay
Engine.GuiInterfaceCall("SetObstructionDebugOverlay", this.checked);
Unit motion overlay
g_Selection.SetMotionDebugOverlay(this.checked);
Game Paused
togglePause();
Settings
Enable Shadows
if (renderer.shadows) this.checked = true; else this.checked = false;
renderer.shadows = this.checked;
Enable Water Reflections
if (renderer.fancyWater) this.checked = true; else this.checked = false;
renderer.fancyWater = this.checked;
Enable Music
if (this.checked) startMusic(); else stopMusic();
Developer Overlay
if (this.checked) toggleDeveloperOverlay(); else toggleDeveloperOverlay();
OK
toggleSettingsWindow();
Settings
toggleSettingsWindow();
Pause Game
togglePause();
Quit Game
toggleMenu();
Menu
toggleMenu();
[stance commands]
[formation commands]
[research commands]
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/FSM.js (revision 7763)
@@ -0,0 +1,244 @@
+// Hierarchical finite state machine implementation.
+//
+// FSMs are specified as a JS data structure;
+// see e.g. AnimalAI.js for an example of the syntax.
+//
+// FSMs are implicitly linked with an external object.
+// That object stores all FSM-related state.
+// (This means we can serialise FSM-based components as
+// plain old JS objects, with no need to serialise the complex
+// FSM structure itself or to add custom serialisation code.)
+
+function FSM(spec)
+{
+ // The (relatively) human-readable FSM specification needs to get
+ // compiled into a more-efficient-to-execute version.
+ //
+ // In particular, message handling should require minimal
+ // property lookups in the common case (even when the FSM has
+ // a deeply nested hierarchy), and there should never be any
+ // string manipulation at run-time.
+
+ this.decompose = { "": [] };
+ /* 'decompose' will store:
+ {
+ "": [],
+ "A": ["A"],
+ "A.B": ["A", "A.B"],
+ "A.B.C": ["A", "A.B", "A.B.C"],
+ "A.B.D": ["A", "A.B", "A.B.D"],
+ ...
+ };
+ This is used when switching between states in different branches
+ of the hierarchy, to determine the list of sub-states to leave/enter
+ */
+
+ this.states = { };
+ /* 'states' will store:
+ {
+ ...
+ "A": {
+ "_name": "A",
+ "_parent": "",
+ "_refs": { // local -> global name lookups (for SetNextState)
+ "B": "A.B",
+ "B.C": "A.B.C",
+ "B.D": "A.B.D",
+ },
+ },
+ "A.B": {
+ "_name": "A.B",
+ "_parent": "A",
+ "_refs": {
+ "C": "A.B.C",
+ "D": "A.B.D",
+ },
+ "MessageType": function(msg) { ... },
+ },
+ "A.B.C": {
+ "_name": "A.B.C",
+ "_parent": "A.B",
+ "_refs": {},
+ "enter": function() { ... },
+ "MessageType": function(msg) { ... },
+ },
+ "A.B.D": {
+ "_name": "A.B.D",
+ "_parent": "A.B",
+ "_refs": {},
+ "enter": function() { ... },
+ "leave": function() { ... },
+ "MessageType": function(msg) { ... },
+ },
+ ...
+ }
+ */
+
+ function process(fsm, node, path, handlers)
+ {
+ var state = {};
+ fsm.states[path.join(".")] = state;
+
+ var newhandlers = {};
+ for (var e in handlers)
+ newhandlers[e] = handlers[e];
+
+ state._name = path.join(".");
+ state._parent = path.slice(0, -1).join(".");
+ state._refs = {};
+
+ for (var key in node)
+ {
+ if (key === "enter" || key === "leave")
+ {
+ state[key] = node[key];
+ }
+ else if (key.match(/^[A-Z]+$/))
+ {
+ state._refs[key] = (state._name ? state._name + "." : "") + key;
+
+ // (the rest of this will be handled later once we've grabbed
+ // all the event handlers)
+ }
+ else
+ {
+ newhandlers[key] = node[key];
+ }
+ }
+
+ for (var e in newhandlers)
+ state[e] = newhandlers[e];
+
+ for (var key in node)
+ {
+ if (key.match(/^[A-Z]+$/))
+ {
+ var newpath = path.concat([key]);
+
+ var decomposed = [newpath[0]];
+ for (var i = 1; i < newpath.length; ++i)
+ decomposed.push(decomposed[i-1] + "." + newpath[i]);
+ fsm.decompose[newpath.join(".")] = decomposed;
+
+ var childstate = process(fsm, node[key], newpath, newhandlers);
+
+ for (var r in childstate._refs)
+ {
+ var cname = key + "." + r;
+ state._refs[cname] = childstate._refs[r];
+ }
+ }
+ }
+
+ return state;
+ }
+
+ process(this, spec, [], {});
+}
+
+FSM.prototype.Init = function(obj, initialState)
+{
+ this.deferFromState = undefined;
+
+ obj.fsmStateName = "";
+ obj.fsmNextState = undefined;
+ this.SwitchToNextState(obj, initialState);
+};
+
+FSM.prototype.SetNextState = function(obj, state)
+{
+ obj.fsmNextState = state;
+};
+
+FSM.prototype.ProcessMessage = function(obj, msg)
+{
+// print("ProcessMessage(obj, "+uneval(msg)+")\n");
+
+ var func = this.states[obj.fsmStateName][msg.type];
+ if (!func)
+ {
+ error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'");
+ return;
+ }
+ func.apply(obj, [msg]);
+
+ while (obj.fsmNextState)
+ {
+ var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState);
+ obj.fsmNextState = undefined;
+
+ if (nextStateName != obj.fsmStateName)
+ this.SwitchToNextState(obj, nextStateName);
+ }
+};
+
+FSM.prototype.DeferMessage = function(obj, msg)
+{
+ // We need to work out which sub-state we were running the message handler from,
+ // and then try again in its parent state.
+ var old = this.deferFromState;
+ var from;
+ if (old) // if we're recursively deferring and saved the last used state, use that
+ from = old;
+ else // if this is the first defer then we must have last processed the message in the current FSM state
+ from = obj.fsmStateName;
+
+ // Find and save the parent, for use in recursive defers
+ this.deferFromState = this.states[from]._parent;
+
+ // Run the function from the parent state
+ var state = this.states[this.deferFromState];
+ var func = state[msg.type];
+ if (!func)
+ error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'");
+ func.apply(obj, [msg]);
+
+ // Restore the changes we made
+ this.deferFromState = old;
+
+ // TODO: if an inherited handler defers, it calls exactly the same handler
+ // on the parent state, which is probably useless and inefficient
+
+ // NOTE: this will break if two units try to execute AI at the same time;
+ // as long as AI messages are queue and processed asynchronously it should be fine
+};
+
+FSM.prototype.LookupState = function(currentStateName, stateName)
+{
+// print("LookupState("+currentStateName+", "+stateName+")\n");
+ for (var s = currentStateName; s; s = this.states[s]._parent)
+ if (stateName in this.states[s]._refs)
+ return this.states[s]._refs[stateName];
+ return stateName;
+};
+
+FSM.prototype.SwitchToNextState = function(obj, nextStateName)
+{
+ var fromState = this.decompose[obj.fsmStateName];
+ var toState = this.decompose[nextStateName];
+
+ if (!toState)
+ error("Tried to change to non-existent state '" + nextState + "'");
+
+ for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix)
+ {
+ }
+
+ for (var i = fromState.length-1; i >= equalPrefix; --i)
+ {
+ var leave = this.states[fromState[i]].leave;
+ if (leave)
+ leave.apply(obj);
+ }
+
+ for (var i = equalPrefix; i < toState.length; ++i)
+ {
+ var enter = this.states[toState[i]].enter;
+ if (enter)
+ enter.apply(obj);
+ }
+
+ obj.fsmStateName = nextStateName;
+}
+
+Engine.RegisterGlobal("FSM", FSM);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Random.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Random.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Random.js (revision 7763)
@@ -0,0 +1,9 @@
+/**
+ * Returns a random integer from min (inclusive) to max (exclusive)
+ */
+function RandomInt(min, max)
+{
+ return Math.floor(min + Math.random() * (max-min))
+}
+
+Engine.RegisterGlobal("RandomInt", RandomInt);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (revision 7762)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (revision 7763)
@@ -1 +1,5 @@
Engine.RegisterInterface("ResourceSupply");
+
+// Message sent from gatherers to ResourceSupply entities
+// when beginning to gather
+Engine.RegisterMessageType("ResourceGather");
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/AnimalAI.js (revision 7763)
@@ -0,0 +1 @@
+Engine.RegisterInterface("AnimalAI");
Index: ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/AnimalAI.js (revision 7763)
@@ -0,0 +1,241 @@
+function AnimalAI() {}
+
+AnimalAI.prototype.Schema =
+ " " +
+ "" +
+ "" +
+ "violent " +
+ "aggressive " +
+ "defensive " +
+ "passive " +
+ "skittish " +
+ " " +
+ " ";
+
+var AnimalFsmSpec = {
+
+ "SKITTISH": {
+
+ "ResourceGather": function(msg) {
+ // If someone's carving chunks of meat off us, then run away
+ this.MoveAwayFrom(msg.gatherer, 12);
+ this.SetNextState("FLEEING");
+ this.PlaySound("panic");
+ },
+
+ "ROAMING": {
+ "enter": function() {
+ // Walk in a random direction
+ this.SelectAnimation("walk", false);
+ this.MoveRandomly();
+ // Set a random timer to switch to feeding state
+ this.StartTimer(RandomInt(2000, 8000));
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "Timer": function(msg) {
+ this.SetNextState("FEEDING");
+ },
+
+ "MoveStopped": function() {
+ this.MoveRandomly();
+ },
+ },
+
+ "FEEDING": {
+ "enter": function() {
+ // Stop and eat for a while
+ this.SelectAnimation("idle");
+ this.StopMoving();
+ this.StartTimer(RandomInt(1000, 4000));
+ },
+
+ "leave": function() {
+ this.StopTimer();
+ },
+
+ "MoveStopped": function() { },
+
+ "Timer": function(msg) {
+ this.SetNextState("ROAMING");
+ },
+ },
+
+ "FLEEING": {
+ "enter": function() {
+ // Run quickly
+ this.SelectAnimation("run", false);
+ this.SetMoveSpeedFactor(6.0);
+ },
+
+ "leave": function() {
+ // Reset normal speed
+ this.SetMoveSpeedFactor(1.0);
+ },
+
+ "MoveStopped": function() {
+ // When we've run far enough, go back to the roaming state
+ this.SetNextState("ROAMING");
+ },
+ },
+ },
+};
+
+var AnimalFsm = new FSM(AnimalFsmSpec);
+
+AnimalAI.prototype.Init = function()
+{
+ this.messageQueue = [];
+};
+
+// FSM linkage functions:
+
+AnimalAI.prototype.OnCreate = function()
+{
+ AnimalFsm.Init(this, "SKITTISH.ROAMING");
+};
+
+AnimalAI.prototype.SetNextState = function(state)
+{
+ AnimalFsm.SetNextState(this, state);
+};
+
+AnimalAI.prototype.DeferMessage = function(msg)
+{
+ AnimalFsm.DeferMessage(this, msg);
+};
+
+AnimalAI.prototype.PushMessage = function(msg)
+{
+ this.messageQueue.push(msg);
+};
+
+AnimalAI.prototype.OnUpdate = function()
+{
+ var mq = this.messageQueue;
+ this.messageQueue = [];
+ for each (var msg in mq)
+ AnimalFsm.ProcessMessage(this, msg);
+};
+
+AnimalAI.prototype.OnMotionChanged = function(msg)
+{
+ if (!msg.speed)
+ this.PushMessage({"type": "MoveStopped"});
+};
+
+AnimalAI.prototype.OnResourceGather = function(msg)
+{
+ this.PushMessage({"type": "ResourceGather", "gatherer": msg.gatherer});
+};
+
+AnimalAI.prototype.TimerHandler = function(data, lateness)
+{
+ this.PushMessage({"type": "Timer", "data": data, "lateness": lateness});
+};
+
+// Functions to be called by the FSM:
+
+AnimalAI.prototype.PlaySound = function(name)
+{
+ PlaySound(name, this.entity);
+};
+
+AnimalAI.prototype.SelectAnimation = function(name, once, speed, sound)
+{
+ var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
+ if (!cmpVisual)
+ return;
+
+ var soundgroup;
+ if (sound)
+ {
+ var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
+ if (cmpSound)
+ soundgroup = cmpSound.GetSoundGroup(sound);
+ }
+
+ // Set default values if unspecified
+ if (typeof once == "undefined")
+ once = false;
+ if (typeof speed == "undefined")
+ speed = 1.0;
+ if (typeof soundgroup == "undefined")
+ soundgroup = "";
+
+ cmpVisual.SelectAnimation(name, once, speed, soundgroup);
+};
+
+AnimalAI.prototype.MoveRandomly = function()
+{
+ // We want to walk in a random direction, but avoid getting stuck
+ // in obstacles or narrow spaces.
+ // So pick a circular range from approximately our current position,
+ // and move outwards to the nearest point on that circle, which will
+ // lead to us avoiding obstacles and moving towards free space.
+
+ // TODO: we probably ought to have a 'home' point, and drift towards
+ // that, so we don't spread out all across the whole map
+
+ var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
+ if (!cmpPosition)
+ return;
+
+ if (!cmpPosition.IsInWorld())
+ return;
+
+ var pos = cmpPosition.GetPosition();
+
+ var distance = 4;
+ var jitter = 0.5;
+
+ // Randomly adjust the range's center a bit, so we tend to prefer
+ // moving in random directions (if there's nothing in the way)
+ var tx = pos.x + (2*Math.random()-1)*jitter;
+ var tz = pos.z + (2*Math.random()-1)*jitter;
+
+ var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ cmpMotion.MoveToPointRange(tx, tz, distance, distance);
+};
+
+AnimalAI.prototype.MoveAwayFrom = function(ent, distance)
+{
+ var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ cmpMotion.MoveToAttackRange(ent, distance, distance);
+};
+
+AnimalAI.prototype.StopMoving = function()
+{
+ var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ cmpMotion.StopMoving();
+};
+
+AnimalAI.prototype.SetMoveSpeedFactor = function(factor)
+{
+ var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
+ cmpMotion.SetSpeedFactor(factor);
+};
+
+AnimalAI.prototype.StartTimer = function(interval, data)
+{
+ if (this.timer)
+ error("Called StartTimer when there's already an active timer");
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.timer = cmpTimer.SetTimeout(this.entity, IID_AnimalAI, "TimerHandler", interval, data);
+};
+
+AnimalAI.prototype.StopTimer = function()
+{
+ if (!this.timer)
+ return;
+
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.timer);
+ this.timer = undefined;
+};
+
+Engine.RegisterComponentType(IID_AnimalAI, "AnimalAI", AnimalAI);
Index: ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7762)
+++ ps/trunk/binaries/data/mods/public/simulation/components/UnitAI.js (revision 7763)
@@ -1,483 +1,486 @@
/*
This is currently just a very simplistic state machine that lets units be commanded around
and then autonomously carry out the orders. It might need to be entirely redesigned.
*/
const STATE_IDLE = 0;
const STATE_WALKING = 1;
const STATE_ATTACKING = 2;
const STATE_REPAIRING = 3;
const STATE_GATHERING = 4;
/* Attack process:
* When starting attack:
* Activate attack animation (with appropriate repeat speed and offset)
* Set this.attackTimer to run at maximum of:
* GetTimers().prepare msec from now
* this.attackRechargeTime
* Loop:
* Wait for the timer
* Perform the attack
* Set this.attackRechargeTime to now plus GetTimers().recharge
* Set this.attackTimer to run after GetTimers().repeat
* At any point it's safe to cancel the attack and switch to a different action
* (The rechargeTime is to prevent people spamming the attack command and getting
* faster-than-normal attacks)
*/
/* Repeat/Gather process is about the same, except with less synchronisation - the action
* is just performed 1sec after initiated, and then repeated every 1sec.
* (TODO: it'd be nice to avoid most of the duplication between Attack and Repeat and Gather code)
*/
function UnitAI() {}
UnitAI.prototype.Schema =
"Controls the unit's movement, attacks, etc, in response to commands from the player. " +
" " +
"" + // TODO: implement this
"" +
"violent " +
"aggressive " +
"defensive " +
"passive " +
"skittish " +
" " +
" ";
UnitAI.prototype.Init = function()
{
this.state = STATE_IDLE;
// The earliest time at which we'll have 'recovered' from the previous attack, and
// can start preparing a new attack
this.attackRechargeTime = 0;
// Timer for AttackTimeout
this.attackTimer = undefined;
// Current attack type
this.attackType = undefined;
// Current target entity ID
this.attackTarget = undefined;
// Timer for RepairTimeout
this.repairTimer = undefined;
// Current target entity ID
this.repairTarget = undefined;
// Timer for GatherTimeout
this.gatherTimer = undefined;
// Current target entity ID
this.gatherTarget = undefined;
};
//// Interface functions ////
UnitAI.prototype.Walk = function(x, z)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (!cmpMotion)
return;
if (cmpMotion.MoveToPoint(x, z))
{
this.state = STATE_WALKING;
PlaySound("walk", this.entity);
}
else
{
this.state = STATE_IDLE;
}
};
UnitAI.prototype.WalkToTarget = function(target)
{
var cmpPosition = Engine.QueryInterface(target, IID_Position);
if (!cmpPosition)
return;
if (!cmpPosition.IsInWorld())
return;
var pos = cmpPosition.GetPosition();
this.Walk(pos.x, pos.z);
}
UnitAI.prototype.Attack = function(target)
{
// Verify that we're able to respond to Attack commands
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
if (!cmpAttack)
{
this.WalkToTarget(target);
return;
}
// TODO: verify that this is a valid target
var type = cmpAttack.GetBestAttack();
if (!type)
{
this.WalkToTarget(target);
return;
}
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.attackType = type;
this.attackTarget = target;
this.state = STATE_ATTACKING;
if (!this.MoveToTarget(target, cmpAttack.GetRange(type)))
{
// We're in range already, do the attack
// (TODO: this could also happen if we couldn't move anywhere)
this.StartAttack();
}
// else we've started moving and the attack will start in OnMotionChanged
};
UnitAI.prototype.Repair = function(target)
{
// Verify that we're able to respond to Repair commands
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
if (!cmpBuilder)
{
this.WalkToTarget(target);
return;
}
// TODO: verify that this is a valid target
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.repairTarget = target;
this.state = STATE_REPAIRING;
if (!this.MoveToTarget(target, cmpBuilder.GetRange()))
{
// We're in range already, do the repairing
// (TODO: this could also happen if we couldn't move anywhere)
this.StartRepair();
}
// else we've started moving and the repair will start in OnMotionChanged
};
UnitAI.prototype.Gather = function(target)
{
// Verify that we're able to respond to Gather commands
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (!cmpResourceGatherer)
{
this.WalkToTarget(target);
return;
}
// Verify that we can gather from this target
if (!cmpResourceGatherer.GetTargetGatherRate(target))
{
this.WalkToTarget(target);
return;
}
// TODO: verify that this is a valid target
// Stop any previous action timers
this.CancelTimers();
// Remember the target, and start moving towards it
this.gatherTarget = target;
this.state = STATE_GATHERING;
if (!this.MoveToTarget(target, cmpResourceGatherer.GetRange()))
{
// We're in range already, do the gathering
// (TODO: this could also happen if we couldn't move anywhere)
this.StartGather();
}
// else we've started moving and the gather will start in OnMotionChanged
};
//// Message handlers ////
UnitAI.prototype.OnDestroy = function()
{
// Clean up any timers that are now obsolete
this.CancelTimers();
};
UnitAI.prototype.OnMotionChanged = function(msg)
{
if (msg.speed)
{
// Started moving
// => play the appropriate animation
this.SelectAnimation("walk", false, msg.speed);
}
else
{
if (this.state == STATE_WALKING)
{
// Stopped walking
this.state = STATE_IDLE;
this.SelectAnimation("idle");
}
else if (this.state == STATE_ATTACKING)
{
// We were attacking, and have stopped moving
// => check if we can still reach the target now
if (this.MoveIntoRange(IID_Attack, this.attackTarget, this.attackType))
return;
// In range, so perform the attack
this.StartAttack();
}
else if (this.state == STATE_REPAIRING)
{
// We were repairing, and have stopped moving
// => check if we can still reach the target now
if (this.MoveIntoRange(IID_Builder, this.repairTarget))
return;
// In range, so perform the repairing
this.StartRepair();
}
else if (this.state == STATE_GATHERING)
{
// We were gathering, and have stopped moving
// => check if we can still reach the target now
if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget))
return;
// In range, so perform the gathering
this.StartGather();
}
}
};
//// Private functions ////
UnitAI.prototype.StartAttack = function()
{
// Perform the attack after the prepare time but not before the previous attack's recharge
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timers = cmpAttack.GetTimers(this.attackType);
var time = Math.max(timers.prepare, this.attackRechargeTime - cmpTimer.GetTime());
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", time, {});
// Start the attack animation and sound, but synced to the timers
this.SelectAnimation("melee", false, 1.0, "attack");
this.SetAnimationSync(time, timers.repeat);
// TODO: this drifts since the sim is quantised to sim turns and these timers aren't
// TODO: we should probably only bother syncing projectile attacks, not melee
};
UnitAI.prototype.StartRepair = function()
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000, {});
// Start the repair/build animation and sound
this.SelectAnimation("build", false, 1.0, "build");
};
UnitAI.prototype.StartGather = function()
{
var cmpResourceSupply = Engine.QueryInterface(this.gatherTarget, IID_ResourceSupply);
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
// Get the animation/sound type name
var type = cmpResourceSupply.GetType();
var typename = "gather_" + (type.specific || type.generic);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000, {"typename": typename});
// Start the gather animation and sound
this.SelectAnimation(typename, false, 1.0, typename);
+
+ // Tell the target we're gathering from it
+ Engine.PostMessage(this.gatherTarget, MT_ResourceGather, { "entity": this.gatherTarget, "gatherer": this.entity });
};
UnitAI.prototype.CancelTimers = function()
{
if (this.attackTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.attackTimer);
this.attackTimer = undefined;
}
if (this.repairTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.repairTimer);
this.repairTimer = undefined;
}
if (this.gatherTimer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.gatherTimer);
this.gatherTimer = undefined;
}
};
/**
* Tries to move into range of the target.
* Returns true if the unit has started walking or on pathing failure, false if already in range.
*/
UnitAI.prototype.MoveIntoRange = function(iid, target, type)
{
var cmpRanged = Engine.QueryInterface(this.entity, iid);
var range = cmpRanged.GetRange(type);
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
if (cmpMotion.IsInAttackRange(target, range.min, range.max))
return false;
// Out of range => need to move closer
// (The target has probably moved while we were chasing it)
if (this.MoveToTarget(target, range))
return true;
// If it's impossible to reach the target, give up
// and switch back to idle
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return true;
};
UnitAI.prototype.SelectAnimation = function(name, once, speed, sound)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
var soundgroup;
if (sound)
{
var cmpSound = Engine.QueryInterface(this.entity, IID_Sound);
if (cmpSound)
soundgroup = cmpSound.GetSoundGroup(sound);
}
// Set default values if unspecified
if (typeof once == "undefined")
once = false;
if (typeof speed == "undefined")
speed = 1.0;
if (typeof soundgroup == "undefined")
soundgroup = "";
cmpVisual.SelectAnimation(name, once, speed, soundgroup);
};
UnitAI.prototype.SetAnimationSync = function(actiontime, repeattime)
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SetAnimationSyncRepeat(repeattime);
cmpVisual.SetAnimationSyncOffset(actiontime);
};
/**
* Tries to move to the specified range of the target.
* This might synchronously trigger a MotionChanged message.
* Returns true if the unit has started walking, false on error or if already in range.
*/
UnitAI.prototype.MoveToTarget = function(target, range)
{
var cmpMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
return cmpMotion.MoveToAttackRange(target, range.min, range.max);
};
UnitAI.prototype.AttackTimeout = function(data, lateness)
{
// If we stopped attacking before this timeout, then don't do any processing here
if (this.state != STATE_ATTACKING)
return;
// Check if we can still reach the target
if (this.MoveIntoRange(IID_Attack, this.attackTarget, this.attackType))
return;
var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
// Hit the target
cmpAttack.PerformAttack(this.attackType, this.attackTarget);
// Set a timer to hit the target again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
var timers = cmpAttack.GetTimers(this.attackType);
this.attackRechargeTime = cmpTimer.GetTime() + timers.recharge;
this.attackTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "AttackTimeout", timers.repeat - lateness, data);
};
UnitAI.prototype.RepairTimeout = function(data, lateness)
{
// If we stopped repairing before this timeout, then don't do any processing here
if (this.state != STATE_REPAIRING)
return;
// Check if we can still reach the target
if (this.MoveIntoRange(IID_Builder, this.repairTarget))
return;
var cmpBuilder = Engine.QueryInterface(this.entity, IID_Builder);
// Repair/build the target
var status = cmpBuilder.PerformBuilding(this.repairTarget);
// If the target is fully built and repaired, then stop and go back to idle
if (status.finished)
{
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return;
}
// Set a timer to gather again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.repairTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "RepairTimeout", 1000 - lateness, data);
};
UnitAI.prototype.GatherTimeout = function(data, lateness)
{
// If we stopped gathering before this timeout, then don't do any processing here
if (this.state != STATE_GATHERING)
return;
// Check if we can still reach the target
if (this.MoveIntoRange(IID_ResourceGatherer, this.gatherTarget))
return;
var cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
// Gather from the target
var status = cmpResourceGatherer.PerformGather(this.gatherTarget);
// If the resource is exhausted, then stop and go back to idle
if (status.exhausted)
{
this.state = STATE_IDLE;
this.SelectAnimation("idle");
return;
}
// Set a timer to gather again
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.gatherTimer = cmpTimer.SetTimeout(this.entity, IID_UnitAI, "GatherTimeout", 1000 - lateness, data);
};
Engine.RegisterComponentType(IID_UnitAI, "UnitAI", UnitAI);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 7762)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/gaia/fauna_chicken.xml (revision 7763)
@@ -1,18 +1,31 @@
gaia
Chicken
40
food.meat
fauna/chicken.xml
1.5
+
+ skittish
+
+
+ 1
+ default
+ default
+
+
+
+ actor/fauna/animal/chickens.xml
+
+
Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 7762)
+++ ps/trunk/binaries/data/config/default.cfg (revision 7763)
@@ -1,213 +1,214 @@
; Global Configuration Settings
;
; **************************************************************
; * DO NOT EDIT THIS FILE if you want personal customisations: *
; * create a text file called "local.cfg" instead, and copy *
; * the lines from this file that you want to change. *
; * *
; * On Linux / OS X, create: *
; * ~/.config/0ad/config/local.cfg *
; * *
; * On Windows, create: *
; * %appdata%/0ad/config/local.cfg *
; * *
; **************************************************************
; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.)
windowed = true
; Force a particular resolution. (If these are 0, the default is
; to keep the current desktop resolution in fullscreen mode or to
; use 1024x768 in windowed mode.)
xres = 0
yres = 0
; Force a non-standard bit depth (if 0 then use the current desktop bit depth)
bpp = 0
; System settings:
fancywater = true
shadows = true
vsync = false
nos3tc = false
noautomipmap = false
novbo = false
noframebufferobject = false
; Specify the render path. This can be one of:
; default Automatically select one of the below, depending on system capabilities
; fixed Only use OpenGL fixed function pipeline
; vertexshader Use vertex shaders for transform and lighting where possible
; Using 'fixed' instead of 'default' may work around some graphics-related problems,
; but will reduce performance when a modern graphics card is available.
renderpath = default
; Adjusts how OpenGL calculates mipmap level of detail. 0.0f is the default (blurry) value.
; Lower values sharpen/extend, and higher values blur/decrease. Clamped at -3.0 to 3.0.
; -1.0 to -1.5 recommended for good results.
lodbias = -0.5
; Profile selection
profile = default
; Language selection: (currently "english" or "pseudogreek")
language = english
; Font mappings:
font.console = console
font.default = palatino12
font.misc = verdana16
; Colour of the sky (in "r g b" format). (Will be removed once there's proper sky support).
;skycolor = "255 0 0"
; Maximum number of players in a session (default 6).
max_players = 8
; GENERAL PREFERENCES:
sound.mastergain = 0.5
; selection.outline.quality = 9; ( higher => very slightly slower, better quality)
; view.scroll.speed = 60
; view.rotate.speed = 0.002
; view.rotate.abouttarget.speed = 0.01
; view.drag.speed = 0.5
; view.zoom.speed = 256.0
; view.zoom.wheel.speed = 16.0
; view.zoom.smoothness = 0.02 ; between 0 and 1
; view.snap.smoothness = 0.02 ; less is sharper, more is softer
; HOTKEY MAPPINGS:
; Each one of the specified keys will trigger the action on the left
; for multiple-key combinations, separate keys with '+' and enclose the entire thing
; in doublequotes.
; See keys.txt for the list of key names.
; > SYSTEM SETTINGS
hotkey.exit = "Ctrl+F4", "Ctrl+Break" ; Exit to desktop.
hotkey.leave = Escape ;End current game or Exit.
hotkey.pause = Pause ; Pause/unpause game.
hotkey.screenshot = F2 ; Take PNG screenshot.
hotkey.bigscreenshot = "Ctrl+Alt+F2" ; Take large BMP screenshot.
hotkey.togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode
hotkey.screenshot.watermark = "K" ; Toggle product/company watermark for official
hotkey.wireframe = "Alt+W" ; Toggle wireframe mode.
hotkey.archive.abort = "Alt+F4" ; Prematurely terminate the archive builder process.
; > CAMERA SETTINGS
hotkey.camera.reset = "H" ; Reset camera rotation to default.
hotkey.camera.reset.origin = "Ctrl+H" ; Reset camera to origin.
hotkey.camera.zoom.in = Plus, Equals, NumPlus ; Zoom camera in.
hotkey.camera.zoom.out = Minus, NumMinus ; Zoom camera out.
hotkey.camera.zoom.wheel.in = WheelUp ; Zoom camera in (wheel speed).
hotkey.camera.zoom.wheel.out = WheelDown ; Zoom camera out (wheel speed).
hotkey.camera.rotate = "Ctrl+MouseMiddle" ; Rotate view by moving mouse, maintaining the
; absolute position of the camera
hotkey.camera.rotate.keyboard = "Shift" ;
hotkey.camera.rotate.abouttarget = "MouseLeft+MouseRight" ; Rotate view by moving mouse, maintaining the
; world coordinates of the centre of the viewport
hotkey.camera.rotate.abouttarget.keyboard = "Ctrl" ;
hotkey.camera.pan = MouseMiddle, ForwardSlash ; Scroll by moving mouse.
hotkey.camera.pan.keyboard = "~Shift+~Ctrl" ; = holding neither Ctrl nor Shift
hotkey.camera.left = A, LeftArrow ; Scroll or rotate left.
hotkey.camera.right = D, RightArrow ; Scroll or rotate right.
hotkey.camera.up = W, UpArrow ; Scroll or rotate up/forwards.
hotkey.camera.down = S, DownArrow ; Scroll or rotate down/backwards.
hotkey.camera.bookmark.0 = F5 ; Saved bookmark 1.
hotkey.camera.bookmark.1 = F6 ; Saved bookmark 2.
hotkey.camera.bookmark.2 = F7 ; Saved bookmark 3.
hotkey.camera.bookmark.3 = F8 ; Saved bookmark 4.
hotkey.camera.bookmark.save = Ctrl ; +bookmark: Save screen as bookmark.
hotkey.camera.bookmark.snap = Alt ; +bookmark: "check up" on bookmark.
;camera.cinema.add = "L"
;camera.cinema.delete = "U"
;camera.cinema.delete.all = "R"
;camera.cinema.write = "O"
camera.cinema.queue = "I"
; > CONSOLE SETTINGS
hotkey.console.toggle = BackQuote, F9 ; Open/close console.
hotkey.console.copy = "Ctrl+C" ; Copy from console to clipboard.
hotkey.console.paste = Insert, "Ctrl+V" ; Paste clipboard to console.
; > ENTITY SELECTION
hotkey.selection.add = Shift ; Add units to selection.
hotkey.selection.remove = Ctrl ; Remove units from selection.
hotkey.selection.group.0 = 0
hotkey.selection.group.1 = 1
hotkey.selection.group.2 = 2
hotkey.selection.group.3 = 3
hotkey.selection.group.4 = 4
hotkey.selection.group.5 = 5
hotkey.selection.group.6 = 6
hotkey.selection.group.7 = 7
hotkey.selection.group.8 = 8
hotkey.selection.group.9 = 9
hotkey.selection.group.add = Shift ; +group: Add units to group.
hotkey.selection.group.save = Ctrl ; +group: Save units to group.
hotkey.selection.group.snap = Alt ; +group: Check up on group.
hotkey.selection.snap = Home ; Centre view on selection.
hotkey.highlightall = O ; Show selection circle for all units on screen.
hotkey.killUnit = Delete
; > CONTEXT CURSOR
hotkey.contextorder.next = LeftBracket ; Cycle right through orders (context cursors).
hotkey.contextorder.previous = RightBracket ; Cycle left through orders (context cursors).
; > TESTING KEYS
hotkey.orbital.toggle = V ; Enable/disable ball arena.
hotkey.orbital.cheat = "LeftCtrl+Backslash" ; Freeze ball.
hotkey.dudemachine.random = "Ctrl+ForwardSlash" ; Create random Dude.
; > OVERLAY KEYS
hotkey.fps.toggle = "Shift+F" ; Toggle frame counter.
hotkey.session.statuspane.toggle = "Shift+S" ; Toggle Status Orb.
hotkey.session.minimap.toggle = "Shift+M" ; Toggle Map Orb.
hotkey.resourcepool.toggle = "Shift+R" ; Toggle Resource Pool.
hotkey.grouppane.toggle = "Shift+G" ; Toggle Group Pane.
hotkey.teamtray.toggle = "Shift+T" ; Toggle Team Tray.
hotkey.session.ShowPlayersList = "Shift+P" ; Toggle Players List
+hotkey.session.devcommands.toggle = "Shift+D"
; > SESSION ORIENTATION KEYS
hotkey.session.gui.flip = "Alt+G" ; Toggle GUI to top/bottom/left/right of screen.
hotkey.session.gui.toggle = "G" ; Toggle visibility of session GUI.
; (TEMPORARY)
hotkey.alwayson.toggle = "Shift+X" ; Toggle always ons (Exit Button, End Game Button).
; > SUBMENU KEYS
hotkey.menu.toggle = "F10" ; Toggle in-game menu.
hotkey.menu.resign = "Alt+X" ; End current game session and return to main menu.
; > HOTKEYS ONLY
hotkey.onlinehelp = "F1" ; Enable/disable online manual entry for current selection.
hotkey.music.toggle = "M" ; Enable/disable music.
hotkey.audio.toggle = "Ctrl+A" ; Enable/disable sound.
; > PROFILER
hotkey.profile.toggle = "F11" ; Enable/disable real-time profiler
hotkey.profile.save = "Shift+F11" ; Save current profiler data to "logs/profile.txt"
; UNMAPPED KEY/MOUSE REFERENCE:
; LMB ; select unit
; RMB ; issue order to selected units
; LMB-drag ; bandbox units
; LMB-doubleclick ; select all units of this type on screen
; LMB-tripleclick ; select all units of this type on map
; shift+mouse select ; add to selection
; ctrl+mouse select ; remove from selection
; #=(1...0) ; select group #, pressing again centres view
; shift+# ; add group # to selection
; ctrl+# ; save selection as group #
; alt+# (hold down) ; snap view to group # (while alt is held down)
; mouse on map edge ; scroll in this direction