Index: ps/trunk/source/simulation2/Simulation2.cpp
===================================================================
--- ps/trunk/source/simulation2/Simulation2.cpp (revision 10587)
+++ ps/trunk/source/simulation2/Simulation2.cpp (revision 10588)
@@ -1,897 +1,898 @@
/* Copyright (C) 2011 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "Simulation2.h"
#include "simulation2/MessageTypes.h"
#include "simulation2/system/ComponentManager.h"
#include "simulation2/system/ParamNode.h"
#include "simulation2/system/SimContext.h"
#include "simulation2/components/ICmpAIManager.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "graphics/MapReader.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "lib/file/vfs/vfs_util.h"
#include "maths/MathUtil.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/Pyrogenesis.h"
#include "ps/XML/Xeromyces.h"
#include
#if MSC_VERSION
#include
#define getpid _getpid // use the non-deprecated function name
#endif
static std::string Hexify(const std::string& s) // TODO: shouldn't duplicate this function in so many places
{
std::stringstream str;
str << std::hex;
for (size_t i = 0; i < s.size(); ++i)
str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i];
return str.str();
}
class CSimulation2Impl
{
public:
CSimulation2Impl(CUnitManager* unitManager, CTerrain* terrain) :
m_SimContext(), m_ComponentManager(m_SimContext),
m_EnableOOSLog(false), m_EnableSerializationTest(false)
{
m_SimContext.m_UnitManager = unitManager;
m_SimContext.m_Terrain = terrain;
m_ComponentManager.LoadComponentTypes();
RegisterFileReloadFunc(ReloadChangedFileCB, this);
// m_EnableOOSLog = true; // TODO: this should be a command-line flag or similar
// m_EnableSerializationTest = true; // TODO: this should too
}
~CSimulation2Impl()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
}
void ResetState(bool skipScriptedComponents, bool skipAI)
{
m_DeltaTime = 0.0;
m_LastFrameOffset = 0.0f;
m_TurnNumber = 0;
ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI);
}
static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI)
{
componentManager.ResetState();
CParamNode noParam;
CComponentManager::ComponentTypeId cid;
// Add native system components:
componentManager.AddComponent(SYSTEM_ENTITY, CID_TemplateManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_CommandQueue, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_ObstructionManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_Pathfinder, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_ProjectileManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_RangeManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_SoundManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_Terrain, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_TerritoryManager, noParam);
componentManager.AddComponent(SYSTEM_ENTITY, CID_WaterManager, noParam);
if (!skipAI)
{
componentManager.AddComponent(SYSTEM_ENTITY, CID_AIManager, noParam);
}
// Add scripted system components:
if (!skipScriptedComponents)
{
#define LOAD_SCRIPTED_COMPONENT(name) \
cid = componentManager.LookupCID(name); \
if (cid == CID__Invalid) \
LOGERROR(L"Can't find component type " L##name); \
componentManager.AddComponent(SYSTEM_ENTITY, cid, noParam)
LOAD_SCRIPTED_COMPONENT("AIInterface");
+ LOAD_SCRIPTED_COMPONENT("Barter");
LOAD_SCRIPTED_COMPONENT("EndGameManager");
LOAD_SCRIPTED_COMPONENT("GuiInterface");
LOAD_SCRIPTED_COMPONENT("PlayerManager");
LOAD_SCRIPTED_COMPONENT("Timer");
#undef LOAD_SCRIPTED_COMPONENT
}
}
static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts);
static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path);
Status ReloadChangedFile(const VfsPath& path);
static Status ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast(param)->ReloadChangedFile(path);
}
int ProgressiveLoad();
void Update(int turnLength, const std::vector& commands);
static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands);
void Interpolate(float frameLength, float frameOffset);
void DumpState();
CSimContext m_SimContext;
CComponentManager m_ComponentManager;
double m_DeltaTime;
float m_LastFrameOffset;
std::wstring m_StartupScript;
CScriptValRooted m_InitAttributes;
CScriptValRooted m_MapSettings;
std::set m_LoadedScripts;
uint32_t m_TurnNumber;
bool m_EnableOOSLog;
// Functions and data for the serialization test mode: (see Update() for relevant comments)
bool m_EnableSerializationTest;
struct SerializationTestState
{
std::stringstream state;
std::stringstream debug;
std::string hash;
};
void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix);
void ReportSerializationFailure(
SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter,
SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter);
static std::vector CloneCommandsFromOtherContext(ScriptInterface& oldScript, ScriptInterface& newScript,
const std::vector& commands)
{
std::vector newCommands = commands;
for (size_t i = 0; i < newCommands.size(); ++i)
{
newCommands[i].data = CScriptValRooted(newScript.GetContext(),
newScript.CloneValueFromOtherContext(oldScript, newCommands[i].data.get()));
}
return newCommands;
}
};
bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts)
{
return (
LoadScripts(componentManager, loadedScripts, "simulation/components/interfaces/") &&
LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") &&
LoadScripts(componentManager, loadedScripts, L"simulation/components/")
);
}
bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path)
{
VfsPaths pathnames;
if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0)
return false;
bool ok = true;
for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it)
{
VfsPath filename = *it;
if (loadedScripts)
loadedScripts->insert(filename);
LOGMESSAGE(L"Loading simulation script '%ls'", filename.string().c_str());
if (! componentManager.LoadScript(filename))
ok = false;
}
return ok;
}
Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path)
{
const VfsPath& filename = path;
// Ignore if this file wasn't loaded as a script
// (TODO: Maybe we ought to load in any new .js files that are created in the right directories)
if (m_LoadedScripts.find(filename) == m_LoadedScripts.end())
return INFO::OK;
// If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message.
// (Also don't bother trying to 'unload' it from the component manager, because that's not possible)
if (!VfsFileExists(path))
return INFO::OK;
LOGMESSAGE(L"Reloading simulation script '%ls'", filename.string().c_str());
if (!m_ComponentManager.LoadScript(filename, true))
return ERR::FAIL;
return INFO::OK;
}
int CSimulation2Impl::ProgressiveLoad()
{
// yield after this time is reached. balances increased progress bar
// smoothness vs. slowing down loading.
const double end_time = timer_Time() + 200e-3;
int ret;
do
{
bool progressed = false;
int total = 0;
int progress = 0;
CMessageProgressiveLoad msg(&progressed, &total, &progress);
m_ComponentManager.BroadcastMessage(msg);
if (!progressed || total == 0)
return 0; // we have nothing left to load
ret = Clamp(100*progress / total, 1, 100);
}
while (timer_Time() < end_time);
return ret;
}
void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix)
{
if (!state.hash.empty())
{
std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc);
file << Hexify(state.hash);
}
if (!state.debug.str().empty())
{
std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc);
file << state.debug.str();
}
if (!state.state.str().empty())
{
std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
file << state.state.str();
}
}
void CSimulation2Impl::ReportSerializationFailure(
SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter,
SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter)
{
OsPath path = psLogDir() / "oos_log";
CreateDirectories(path, 0700);
// Clean up obsolete files from previous runs
wunlink(path / "hash.before.a");
wunlink(path / "hash.before.b");
wunlink(path / "debug.before.a");
wunlink(path / "debug.before.b");
wunlink(path / "state.before.a");
wunlink(path / "state.before.b");
wunlink(path / "hash.after.a");
wunlink(path / "hash.after.b");
wunlink(path / "debug.after.a");
wunlink(path / "debug.after.b");
wunlink(path / "state.after.a");
wunlink(path / "state.after.b");
if (primaryStateBefore)
DumpSerializationTestState(*primaryStateBefore, path, L"before.a");
if (primaryStateAfter)
DumpSerializationTestState(*primaryStateAfter, path, L"after.a");
if (secondaryStateBefore)
DumpSerializationTestState(*secondaryStateBefore, path, L"before.b");
if (secondaryStateAfter)
DumpSerializationTestState(*secondaryStateAfter, path, L"after.b");
debug_warn(L"Serialization test failure");
}
void CSimulation2Impl::Update(int turnLength, const std::vector& commands)
{
PROFILE3("sim update");
PROFILE2_ATTR("turn %d", (int)m_TurnNumber);
fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000;
/*
* In serialization test mode, we save the original (primary) simulation state before each turn update.
* We run the update, then load the saved state into a secondary context.
* We serialize that again and compare to the original serialization (to check that
* serialize->deserialize->serialize is equivalent to serialize).
* Then we run the update on the secondary context, and check that its new serialized
* state matches the primary context after the update (to check that the simulation doesn't depend
* on anything that's not serialized).
*/
const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow)
const bool serializationTestHash = true; // set true to save and compare hash of state
SerializationTestState primaryStateBefore;
if (m_EnableSerializationTest)
{
ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state));
if (serializationTestDebugDump)
ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false));
if (serializationTestHash)
ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false));
}
UpdateComponents(m_SimContext, turnLengthFixed, commands);
if (m_EnableSerializationTest)
{
// Initialise the secondary simulation
CTerrain secondaryTerrain;
CSimContext secondaryContext;
secondaryContext.m_Terrain = &secondaryTerrain;
CComponentManager secondaryComponentManager(secondaryContext);
secondaryComponentManager.LoadComponentTypes();
ENSURE(LoadDefaultScripts(secondaryComponentManager, NULL));
ResetComponentState(secondaryComponentManager, false, false);
// Load the map into the secondary simulation
LDR_BeginRegistering();
CMapReader* mapReader = new CMapReader; // automatically deletes itself
// TODO: this duplicates CWorld::RegisterInit and could probably be cleaned up a bit
std::string mapType;
m_ComponentManager.GetScriptInterface().GetProperty(m_InitAttributes.get(), "mapType", mapType);
if (mapType == "scenario")
{
// Load scenario attributes
std::wstring mapFile;
m_ComponentManager.GetScriptInterface().GetProperty(m_InitAttributes.get(), "map", mapFile);
VfsPath mapfilename(VfsPath("maps/scenarios") / (mapFile + L".pmp"));
mapReader->LoadMap(mapfilename, &secondaryTerrain, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, &secondaryContext, INVALID_PLAYER, true); // throws exception on failure
}
else
{
// TODO: support random map scripts
debug_warn(L"Serialization test mode only supports scenarios");
}
LDR_EndRegistering();
ENSURE(LDR_NonprogressiveLoad() == INFO::OK);
ENSURE(secondaryComponentManager.DeserializeState(primaryStateBefore.state));
SerializationTestState secondaryStateBefore;
ENSURE(secondaryComponentManager.SerializeState(secondaryStateBefore.state));
if (serializationTestDebugDump)
ENSURE(secondaryComponentManager.DumpDebugState(secondaryStateBefore.debug, false));
if (serializationTestHash)
ENSURE(secondaryComponentManager.ComputeStateHash(secondaryStateBefore.hash, false));
if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() ||
primaryStateBefore.hash != secondaryStateBefore.hash)
{
ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL);
}
SerializationTestState primaryStateAfter;
ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state));
if (serializationTestHash)
ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false));
UpdateComponents(secondaryContext, turnLengthFixed,
CloneCommandsFromOtherContext(m_ComponentManager.GetScriptInterface(), secondaryComponentManager.GetScriptInterface(), commands));
SerializationTestState secondaryStateAfter;
ENSURE(secondaryComponentManager.SerializeState(secondaryStateAfter.state));
if (serializationTestHash)
ENSURE(secondaryComponentManager.ComputeStateHash(secondaryStateAfter.hash, false));
if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() ||
primaryStateAfter.hash != secondaryStateAfter.hash)
{
// Only do the (slow) dumping now we know we're going to need to report it
ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false));
ENSURE(secondaryComponentManager.DumpDebugState(secondaryStateAfter.debug, false));
ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter);
}
}
// if (m_TurnNumber == 0)
// m_ComponentManager.GetScriptInterface().DumpHeap();
// Run the GC occasionally
// (TODO: we ought to schedule this for a frame where we're not
// running the sim update, to spread the load)
if (m_TurnNumber % 10 == 0)
m_ComponentManager.GetScriptInterface().MaybeGC();
if (m_EnableOOSLog)
DumpState();
// Start computing AI for the next turn
CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY);
if (!cmpAIManager.null())
cmpAIManager->StartComputation();
++m_TurnNumber;
}
void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands)
{
// TODO: the update process is pretty ugly, with lots of messages and dependencies
// between different components. Ought to work out a nicer way to do this.
CComponentManager& componentManager = simContext.GetComponentManager();
CMessageTurnStart msgTurnStart;
componentManager.BroadcastMessage(msgTurnStart);
CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY);
if (!cmpPathfinder.null())
cmpPathfinder->FinishAsyncRequests();
// Push AI commands onto the queue before we use them
CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY);
if (!cmpAIManager.null())
cmpAIManager->PushCommands();
CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY);
if (!cmpCommandQueue.null())
cmpCommandQueue->FlushTurn(commands);
// Process newly generated move commands so the UI feels snappy
if (!cmpPathfinder.null())
cmpPathfinder->ProcessSameTurnMoves();
// Send all the update phases
{
CMessageUpdate msgUpdate(turnLengthFixed);
componentManager.BroadcastMessage(msgUpdate);
}
{
CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed);
componentManager.BroadcastMessage(msgUpdate);
}
// Process move commands for formations (group proxy)
if (!cmpPathfinder.null())
cmpPathfinder->ProcessSameTurnMoves();
{
CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed);
componentManager.BroadcastMessage(msgUpdate);
}
{
CMessageUpdate_Final msgUpdate(turnLengthFixed);
componentManager.BroadcastMessage(msgUpdate);
}
// Process moves resulting from group proxy movement (unit needs to catch up or realign) and any others
if (!cmpPathfinder.null())
cmpPathfinder->ProcessSameTurnMoves();
// Clean up any entities destroyed during the simulation update
componentManager.FlushDestroyedComponents();
}
void CSimulation2Impl::Interpolate(float frameLength, float frameOffset)
{
PROFILE3("sim interpolate");
m_LastFrameOffset = frameOffset;
CMessageInterpolate msg(frameLength, frameOffset);
m_ComponentManager.BroadcastMessage(msg);
// Clean up any entities destroyed during interpolate (e.g. local corpses)
m_ComponentManager.FlushDestroyedComponents();
}
void CSimulation2Impl::DumpState()
{
PROFILE("DumpState");
std::stringstream pid;
pid << getpid();
std::stringstream name;\
name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt";
OsPath path = psLogDir() / "sim_log" / pid.str() / name.str();
CreateDirectories(path.Parent(), 0700);
std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc);
file << "State hash: " << std::hex;
std::string hashRaw;
m_ComponentManager.ComputeStateHash(hashRaw, false);
for (size_t i = 0; i < hashRaw.size(); ++i)
file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i];
file << std::dec << "\n";
file << "\n";
m_ComponentManager.DumpDebugState(file, true);
std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
m_ComponentManager.SerializeState(binfile);
}
////////////////////////////////////////////////////////////////
CSimulation2::CSimulation2(CUnitManager* unitManager, CTerrain* terrain) :
m(new CSimulation2Impl(unitManager, terrain))
{
}
CSimulation2::~CSimulation2()
{
delete m;
}
// Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods:
void CSimulation2::EnableOOSLog()
{
m->m_EnableOOSLog = true;
}
void CSimulation2::EnableSerializationTest()
{
m->m_EnableSerializationTest = true;
}
entity_id_t CSimulation2::AddEntity(const std::wstring& templateName)
{
return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity());
}
entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId)
{
return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId));
}
entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName)
{
return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity());
}
void CSimulation2::DestroyEntity(entity_id_t ent)
{
m->m_ComponentManager.DestroyComponentsSoon(ent);
}
void CSimulation2::FlushDestroyedEntities()
{
m->m_ComponentManager.FlushDestroyedComponents();
}
IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const
{
return m->m_ComponentManager.QueryInterface(ent, iid);
}
void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const
{
m->m_ComponentManager.PostMessage(ent, msg);
}
void CSimulation2::BroadcastMessage(const CMessage& msg) const
{
m->m_ComponentManager.BroadcastMessage(msg);
}
CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid)
{
return m->m_ComponentManager.GetEntitiesWithInterface(iid);
}
const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid)
{
return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid);
}
const CSimContext& CSimulation2::GetSimContext() const
{
return m->m_SimContext;
}
ScriptInterface& CSimulation2::GetScriptInterface() const
{
return m->m_ComponentManager.GetScriptInterface();
}
void CSimulation2::InitGame(const CScriptVal& data)
{
CScriptVal ret; // ignored
GetScriptInterface().CallFunction(GetScriptInterface().GetGlobalObject(), "InitGame", data, ret);
}
void CSimulation2::Update(int turnLength)
{
std::vector commands;
m->Update(turnLength, commands);
}
void CSimulation2::Update(int turnLength, const std::vector& commands)
{
m->Update(turnLength, commands);
}
void CSimulation2::Interpolate(float frameLength, float frameOffset)
{
m->Interpolate(frameLength, frameOffset);
}
void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling)
{
PROFILE3("sim submit");
CMessageRenderSubmit msg(collector, frustum, culling);
m->m_ComponentManager.BroadcastMessage(msg);
}
float CSimulation2::GetLastFrameOffset() const
{
return m->m_LastFrameOffset;
}
bool CSimulation2::LoadScripts(const VfsPath& path)
{
return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path);
}
bool CSimulation2::LoadDefaultScripts()
{
return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts);
}
void CSimulation2::SetStartupScript(const std::wstring& code)
{
m->m_StartupScript = code;
}
const std::wstring& CSimulation2::GetStartupScript()
{
return m->m_StartupScript;
}
void CSimulation2::SetInitAttributes(const CScriptValRooted& attribs)
{
m->m_InitAttributes = attribs;
}
CScriptValRooted CSimulation2::GetInitAttributes()
{
return m->m_InitAttributes;
}
void CSimulation2::SetMapSettings(const std::string& settings)
{
m->m_MapSettings = m->m_ComponentManager.GetScriptInterface().ParseJSON(settings);
}
void CSimulation2::SetMapSettings(const CScriptValRooted& settings)
{
m->m_MapSettings = settings;
}
std::string CSimulation2::GetMapSettingsString()
{
return m->m_ComponentManager.GetScriptInterface().StringifyJSON(m->m_MapSettings.get());
}
CScriptVal CSimulation2::GetMapSettings()
{
return m->m_MapSettings.get();
}
void CSimulation2::LoadPlayerSettings(bool newPlayers)
{
GetScriptInterface().CallFunctionVoid(GetScriptInterface().GetGlobalObject(), "LoadPlayerSettings", m->m_MapSettings, newPlayers);
}
void CSimulation2::LoadMapSettings()
{
// Initialize here instead of in Update()
GetScriptInterface().CallFunctionVoid(GetScriptInterface().GetGlobalObject(), "LoadMapSettings", m->m_MapSettings);
if (!m->m_StartupScript.empty())
GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript);
}
int CSimulation2::ProgressiveLoad()
{
return m->ProgressiveLoad();
}
Status CSimulation2::ReloadChangedFile(const VfsPath& path)
{
return m->ReloadChangedFile(path);
}
void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI)
{
m->ResetState(skipScriptedComponents, skipAI);
}
bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick)
{
return m->m_ComponentManager.ComputeStateHash(outHash, quick);
}
bool CSimulation2::DumpDebugState(std::ostream& stream)
{
return m->m_ComponentManager.DumpDebugState(stream, true);
}
bool CSimulation2::SerializeState(std::ostream& stream)
{
return m->m_ComponentManager.SerializeState(stream);
}
bool CSimulation2::DeserializeState(std::istream& stream)
{
// TODO: need to make sure the required SYSTEM_ENTITY components get constructed
return m->m_ComponentManager.DeserializeState(stream);
}
std::string CSimulation2::GenerateSchema()
{
return m->m_ComponentManager.GenerateSchema();
}
std::vector CSimulation2::GetRMSData()
{
VfsPath path(L"maps/random/");
VfsPaths pathnames;
std::vector data;
// Find all ../maps/random/*.json
Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames);
if (ret == INFO::OK)
{
for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it)
{
// Load JSON file
CVFSFile file;
PSRETURN ret = file.Load(g_VFS, *it);
if (ret != PSRETURN_OK)
{
LOGERROR(L"Failed to load file '%ls': %hs", path.string().c_str(), GetErrorString(ret));
}
else
{
data.push_back(std::string(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize()));
}
}
}
else
{
// Some error reading directory
wchar_t error[200];
LOGERROR(L"Error reading directory '%ls': %ls", path.string().c_str(), StatusDescription(ret, error, ARRAY_SIZE(error)));
}
return data;
}
std::vector CSimulation2::GetCivData()
{
VfsPath path(L"civs/");
VfsPaths pathnames;
std::vector data;
// Load all JSON files in civs directory
Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames);
if (ret == INFO::OK)
{
for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it)
{
// Load JSON file
CVFSFile file;
PSRETURN ret = file.Load(g_VFS, *it);
if (ret != PSRETURN_OK)
{
LOGERROR(L"CSimulation2::GetCivData: Failed to load file '%ls': %hs", path.string().c_str(), GetErrorString(ret));
}
else
{
data.push_back(std::string(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize()));
}
}
}
else
{
// Some error reading directory
wchar_t error[200];
LOGERROR(L"CSimulation2::GetCivData: Error reading directory '%ls': %ls", path.string().c_str(), StatusDescription(ret, error, ARRAY_SIZE(error)));
}
return data;
}
std::string CSimulation2::GetPlayerDefaults()
{
return ReadJSON(L"simulation/data/player_defaults.json");
}
std::string CSimulation2::GetMapSizes()
{
return ReadJSON(L"simulation/data/map_sizes.json");
}
std::string CSimulation2::ReadJSON(VfsPath path)
{
std::string data;
if (!VfsFileExists(path))
{
LOGERROR(L"File '%ls' does not exist", path.string().c_str());
}
else
{
// Load JSON file
CVFSFile file;
PSRETURN ret = file.Load(g_VFS, path);
if (ret != PSRETURN_OK)
{
LOGERROR(L"Failed to load file '%ls': %hs", path.string().c_str(), GetErrorString(ret));
}
else
{
data = std::string(file.GetBuffer(), file.GetBuffer() + file.GetBufferSize());
}
}
return data;
}
std::string CSimulation2::GetAIData()
{
ScriptInterface& scriptInterface = GetScriptInterface();
std::vector aiData = ICmpAIManager::GetAIs(scriptInterface);
// Build single JSON string with array of AI data
CScriptValRooted ais;
if (!scriptInterface.Eval("({})", ais) || !scriptInterface.SetProperty(ais.get(), "AIData", aiData))
return std::string();
return scriptInterface.StringifyJSON(ais.get());
}
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 10587)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 10588)
@@ -1,772 +1,797 @@
Return to Main MenuleaveGame()
Index: ps/trunk/binaries/data/mods/public/gui/session/input.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 10587)
+++ ps/trunk/binaries/data/mods/public/gui/session/input.js (revision 10588)
@@ -1,1243 +1,1250 @@
const SDL_BUTTON_LEFT = 1;
const SDL_BUTTON_MIDDLE = 2;
const SDL_BUTTON_RIGHT = 3;
const SDLK_LEFTBRACKET = 91;
const SDLK_RIGHTBRACKET = 93;
const SDLK_RSHIFT = 303;
const SDLK_LSHIFT = 304;
const SDLK_RCTRL = 305;
const SDLK_LCTRL = 306;
const SDLK_RALT = 307;
const SDLK_LALT = 308;
// TODO: these constants should be defined somewhere else instead, in
// case any other code wants to use them too
const ACTION_NONE = 0;
const ACTION_GARRISON = 1;
const ACTION_REPAIR = 2;
var preSelectedAction = ACTION_NONE;
var INPUT_NORMAL = 0;
var INPUT_SELECTING = 1;
var INPUT_BANDBOXING = 2;
var INPUT_BUILDING_PLACEMENT = 3;
var INPUT_BUILDING_CLICK = 4;
var INPUT_BUILDING_DRAG = 5;
var INPUT_BATCHTRAINING = 6;
var INPUT_PRESELECTEDACTION = 7;
var inputState = INPUT_NORMAL;
var defaultPlacementAngle = Math.PI*3/4;
var placementAngle = undefined;
var placementPosition = undefined;
var placementEntity = undefined;
var mouseX = 0;
var mouseY = 0;
var mouseIsOverObject = false;
// Number of pixels the mouse can move before the action is considered a drag
var maxDragDelta = 4;
// Time in milliseconds in which a double click is recognized
const doubleClickTime = 500;
var doubleClickTimer = 0;
var doubleClicked = false;
// Store the previously clicked entity - ensure a double/triple click happens on the same entity
var prevClickedEntity = 0;
// Same double-click behaviour for hotkey presses
const doublePressTime = 500;
var doublePressTimer = 0;
var prevHotkey = 0;
function updateCursor()
{
if (!mouseIsOverObject)
{
var action = determineAction(mouseX, mouseY);
if (inputState == INPUT_NORMAL || inputState == INPUT_PRESELECTEDACTION)
{
if (action)
{
if (action.cursor)
{
Engine.SetCursor(action.cursor);
return;
}
}
}
}
Engine.SetCursor("arrow-default");
}
function updateBuildingPlacementPreview()
{
// The preview should be recomputed every turn, so that it responds
// to obstructions/fog/etc moving underneath it
if (placementEntity && placementPosition)
{
return Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z,
"angle": placementAngle
});
}
return false;
}
function resetPlacementEntity()
{
Engine.GuiInterfaceCall("SetBuildingPlacementPreview", {"template": ""});
placementEntity = undefined;
placementPosition = undefined;
placementAngle = undefined;
}
function findGatherType(gatherer, supply)
{
if (!gatherer || !supply)
return undefined;
if (gatherer[supply.type.generic+"."+supply.type.specific])
return supply.type.specific;
if (gatherer[supply.type.generic])
return supply.type.generic;
return undefined;
}
function getActionInfo(action, target)
{
var selection = g_Selection.toList();
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return {"possible": false};
// If the selection isn't friendly units, no action
var playerID = Engine.GetPlayerID();
var allOwnedByPlayer = selection.every(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.player == playerID;
});
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return {"possible": false};
// Work out whether the selection can have rally points
var haveRallyPoints = selection.every(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.rallyPoint;
});
if (!target)
{
if (action == "set-rallypoint" && haveRallyPoints)
return {"possible": true};
else if (action == "move")
return {"possible": true};
else
return {"possible": false};
}
if (haveRallyPoints && selection.indexOf(target) != -1 && action == "unset-rallypoint")
return {"possible": true};
// Look at the first targeted entity
// (TODO: maybe we eventually want to look at more, and be more context-sensitive?
// e.g. prefer to attack an enemy unit, even if some friendly units are closer to the mouse)
var targetState = GetEntityState(target);
// If we selected buildings with rally points, and then click on one of those selected
// buildings, we should remove the rally point
//if (haveRallyPoints && selection.indexOf(target) != -1)
// return {"type": "unset-rallypoint"};
// Check if the target entity is a resource, dropsite, foundation, or enemy unit.
// Check if any entities in the selection can gather the requested resource,
// can return to the dropsite, can build the foundation, or can attack the enemy
var simState = Engine.GuiInterfaceCall("GetSimulationState");
for each (var entityID in selection)
{
var entState = GetEntityState(entityID);
if (!entState)
continue;
var playerState = simState.players[entState.player];
var playerOwned = (targetState.player == entState.player);
var allyOwned = playerState.isAlly[targetState.player];
var enemyOwned = playerState.isEnemy[targetState.player];
var gaiaOwned = (targetState.player == 0);
// Find the resource type we're carrying, if any
var carriedType = undefined;
if (entState.resourceCarrying && entState.resourceCarrying.length)
carriedType = entState.resourceCarrying[0].type;
switch (action)
{
case "garrison":
if (isUnit(entState) && targetState.garrisonHolder && playerOwned)
{
var allowedClasses = targetState.garrisonHolder.allowedClasses;
for each (var unitClass in entState.identity.classes)
{
if (allowedClasses.indexOf(unitClass) != -1)
{
return {"possible": true};
}
}
}
break;
case "gather":
if (targetState.resourceSupply && (playerOwned || gaiaOwned))
{
var resource = findGatherType(entState.resourceGatherRates, targetState.resourceSupply);
if (resource)
return {"possible": true, "cursor": "action-gather-" + resource};
}
break;
case "returnresource":
if (targetState.resourceDropsite && playerOwned && carriedType && targetState.resourceDropsite.types.indexOf(carriedType) != -1)
return {"possible": true, "cursor": "action-return-" + carriedType};
break;
case "build":
if (targetState.foundation && entState.buildEntities && playerOwned)
return {"possible": true};
break;
case "repair":
if (entState.buildEntities && targetState.needsRepair && allyOwned)
return {"possible": true};
break;
case "attack":
if (entState.attack && targetState.hitpoints && enemyOwned)
return {"possible": true};
}
}
if (action == "move")
return {"possible": true};
else
return {"possible": false};
}
/**
* Determine the context-sensitive action that should be performed when the mouse is at (x,y)
*/
function determineAction(x, y, fromMinimap)
{
var selection = g_Selection.toList();
// No action if there's no selection
if (!selection.length)
{
preSelectedAction = ACTION_NONE;
return undefined;
}
// If the selection doesn't exist, no action
var entState = GetEntityState(selection[0]);
if (!entState)
return undefined;
// If the selection isn't friendly units, no action
var playerID = Engine.GetPlayerID();
var allOwnedByPlayer = selection.every(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.player == playerID;
});
if (!g_DevSettings.controlAll && !allOwnedByPlayer)
return undefined;
// Work out whether the selection can have rally points
var haveRallyPoints = selection.every(function(ent) {
var entState = GetEntityState(ent);
return entState && entState.rallyPoint;
});
var targets = [];
var target = undefined;
var type = "none";
var cursor = "";
var targetState = undefined;
if (!fromMinimap)
targets = Engine.PickEntitiesAtPoint(x, y);
if (targets.length)
{
target = targets[0];
}
if (preSelectedAction != ACTION_NONE)
{
switch (preSelectedAction)
{
case ACTION_GARRISON:
if (getActionInfo("garrison", target).possible)
return {"type": "garrison", "cursor": "action-garrison", "target": target};
else
return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined};
break;
case ACTION_REPAIR:
if (getActionInfo("repair", target).possible)
return {"type": "repair", "cursor": "action-repair", "target": target};
else
return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
break;
}
}
else if (Engine.HotkeyIsPressed("session.garrison"))
{
if (getActionInfo("garrison", target).possible)
return {"type": "garrison", "cursor": "action-garrison", "target": target};
else
return {"type": "none", "cursor": "action-garrison-disabled", "target": undefined};
}
else
{
var actionInfo = undefined;
if ((actionInfo = getActionInfo("gather", target)).possible)
return {"type": "gather", "cursor": actionInfo.cursor, "target": target};
else if ((actionInfo = getActionInfo("returnresource", target)).possible)
return {"type": "returnresource", "cursor": actionInfo.cursor, "target": target};
else if (getActionInfo("build", target).possible)
return {"type": "build", "cursor": "action-build", "target": target};
else if (getActionInfo("repair", target).possible)
return {"type": "build", "cursor": "action-repair", "target": target};
else if (getActionInfo("attack", target).possible)
return {"type": "attack", "cursor": "action-attack", "target": target};
else if(getActionInfo("set-rallypoint", target).possible)
return {"type": "set-rallypoint"};
else if(getActionInfo("unset-rallypoint", target).possible)
return {"type": "unset-rallypoint"};
else if (getActionInfo("move", target).possible)
return {"type": "move"};
}
return {"type": type, "cursor": cursor, "target": target};
}
var dragStart; // used for remembering mouse coordinates at start of drag operations
function tryPlaceBuilding(queued)
{
var selection = g_Selection.toList();
// Use the preview to check it's a valid build location
if (!updateBuildingPlacementPreview())
{
// invalid location - don't build it
// TODO: play a sound?
return false;
}
// Start the construction
Engine.PostNetworkCommand({
"type": "construct",
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z,
"angle": placementAngle,
"entities": selection,
"autorepair": true,
"autocontinue": true,
"queued": queued
});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
if (!queued)
resetPlacementEntity();
return true;
}
// Limits bandboxed selections to certain types of entities based on priority
function getPreferredEntities(ents)
{
var entStateList = [];
var preferredEnts = [];
// Check if there are units in the selection and get a list of entity states
for each (var ent in ents)
{
var entState = GetEntityState(ent);
if (!entState)
continue;
if (isUnit(entState))
preferredEnts.push(ent);
entStateList.push(entState);
}
// If there are no units, check if there are defensive entities in the selection
if (!preferredEnts.length)
for (var i = 0; i < ents.length; i++)
if (isDefensive(entStateList[i]))
preferredEnts.push(ents[i]);
return preferredEnts;
}
function handleInputBeforeGui(ev, hoveredObject)
{
// Capture mouse position so we can use it for displaying cursors,
// and key states
switch (ev.type)
{
case "mousebuttonup":
case "mousebuttondown":
case "mousemotion":
mouseX = ev.x;
mouseY = ev.y;
break;
}
// Remember whether the mouse is over a GUI object or not
mouseIsOverObject = (hoveredObject != null);
// State-machine processing:
//
// (This is for states which should override the normal GUI processing - events will
// be processed here before being passed on, and propagation will stop if this function
// returns true)
//
// TODO: it'd probably be nice to have a better state-machine system, with guaranteed
// entry/exit functions, since this is a bit broken now
switch (inputState)
{
case INPUT_BANDBOXING:
switch (ev.type)
{
case "mousemotion":
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = getGUIObjectByName("bandbox");
bandbox.size = [x0, y0, x1, y1].join(" ");
bandbox.hidden = false;
// TODO: Should we handle "control all units" here as well?
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var x0 = dragStart[0];
var y0 = dragStart[1];
var x1 = ev.x;
var y1 = ev.y;
if (x0 > x1) { var t = x0; x0 = x1; x1 = t; }
if (y0 > y1) { var t = y0; y0 = y1; y1 = t; }
var bandbox = getGUIObjectByName("bandbox");
bandbox.hidden = true;
// Get list of entities limited to preferred entities
// TODO: Should we handle "control all units" here as well?
var ents = Engine.PickFriendlyEntitiesInRect(x0, y0, x1, y1, Engine.GetPlayerID());
var preferredEntities = getPreferredEntities(ents)
if (preferredEntities.length)
ents = preferredEntities;
// Remove the bandbox hover highlighting
g_Selection.setHighlightList([]);
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel selection
var bandbox = getGUIObjectByName("bandbox");
bandbox.hidden = true;
g_Selection.setHighlightList([]);
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_CLICK:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved far enough from the original click location,
// then switch to drag-orientation mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BUILDING_DRAG;
return false;
}
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
resetPlacementEntity();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_DRAG:
switch (ev.type)
{
case "mousemotion":
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
var maxDragDelta = 16;
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
// Rotate in the direction of the mouse
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
placementAngle = Math.atan2(target.x - placementPosition.x, target.z - placementPosition.z);
}
else
{
// If the mouse is near the center, snap back to the default orientation
placementAngle = defaultPlacementAngle;
}
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z
});
if (snapData)
{
placementAngle = snapData.angle;
placementPosition.x = snapData.x;
placementPosition.z = snapData.z;
}
updateBuildingPlacementPreview();
break;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
// If shift is down, let the player continue placing another of the same building
var queued = Engine.HotkeyIsPressed("session.queue");
if (tryPlaceBuilding(queued))
{
if (queued)
inputState = INPUT_BUILDING_PLACEMENT;
else
inputState = INPUT_NORMAL;
}
else
{
inputState = INPUT_BUILDING_PLACEMENT;
}
return true;
}
break;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
resetPlacementEntity();
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BATCHTRAINING:
switch (ev.type)
{
case "hotkeyup":
if (ev.hotkey == "session.batchtrain")
{
flushTrainingQueueBatch();
inputState = INPUT_NORMAL;
}
break;
}
}
return false;
}
function handleInputAfterGui(ev)
{
// Handle the time-warp testing features, restricted to single-player
if (!g_IsNetworked && getGUIObjectByName("devTimeWarp").checked)
{
if (ev.type == "hotkeydown" && ev.hotkey == "timewarp.fastforward")
Engine.SetSimRate(20.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.fastforward")
Engine.SetSimRate(1.0);
else if (ev.type == "hotkeyup" && ev.hotkey == "timewarp.rewind")
Engine.RewindTimeWarp();
}
// State-machine processing:
switch (inputState)
{
case INPUT_NORMAL:
switch (ev.type)
{
case "mousemotion":
// Highlight the first hovered entity (if any)
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
if (ents.length)
g_Selection.setHighlightList([ents[0]]);
else
g_Selection.setHighlightList([]);
return false;
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
dragStart = [ ev.x, ev.y ];
inputState = INPUT_SELECTING;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
return doAction(action, ev);
}
break;
case "hotkeydown":
if (ev.hotkey.indexOf("selection.group.") == 0)
{
var now = new Date();
if ((now.getTime() - doublePressTimer < doublePressTime) && (ev.hotkey == prevHotkey))
{
if (ev.hotkey.indexOf("selection.group.select.") == 0)
{
var sptr = ev.hotkey.split(".");
performGroup("snap", sptr[3]);
}
}
else
{
var sptr = ev.hotkey.split(".");
performGroup(sptr[2], sptr[3]);
doublePressTimer = now.getTime();
prevHotkey = ev.hotkey;
}
}
break;
}
break;
case INPUT_PRESELECTEDACTION:
switch (ev.type)
{
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
{
var action = determineAction(ev.x, ev.y);
if (!action)
break;
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
return doAction(action, ev);
}
else if (ev.button == SDL_BUTTON_RIGHT && preSelectedAction != ACTION_NONE)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
// else
default:
// Slight hack: If selection is empty, reset the input state
if (g_Selection.toList().length == 0)
{
preSelectedAction = ACTION_NONE;
inputState = INPUT_NORMAL;
break;
}
}
break;
case INPUT_SELECTING:
switch (ev.type)
{
case "mousemotion":
// If the mouse moved further than a limit, switch to bandbox mode
var dragDeltaX = ev.x - dragStart[0];
var dragDeltaY = ev.y - dragStart[1];
if (Math.abs(dragDeltaX) >= maxDragDelta || Math.abs(dragDeltaY) >= maxDragDelta)
{
inputState = INPUT_BANDBOXING;
return false;
}
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
g_Selection.setHighlightList(ents);
return false;
case "mousebuttonup":
if (ev.button == SDL_BUTTON_LEFT)
{
var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
if (!ents.length)
{
g_Selection.reset();
resetIdleUnit();
inputState = INPUT_NORMAL;
return true;
}
var selectedEntity = ents[0];
var now = new Date();
// If camera following and we select different unit, stop
if (Engine.GetFollowedEntity() != selectedEntity)
{
Engine.CameraFollow(0);
}
if ((now.getTime() - doubleClickTimer < doubleClickTime) && (selectedEntity == prevClickedEntity))
{
// Double click or triple click has occurred
var showOffscreen = Engine.HotkeyIsPressed("selection.offscreen");
var matchRank = true;
var templateToMatch;
// Check for double click or triple click
if (!doubleClicked)
{
// If double click hasn't already occurred, this is a double click.
// Select units matching exact template name (same rank)
templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template;
doubleClicked = true;
// Reset the timer so the user has an extra period 'doubleClickTimer' to do a triple-click
doubleClickTimer = now.getTime();
}
else
{
// Double click has already occurred, so this is a triple click.
// Select similar units regardless of rank
templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).identity.selectionGroupName;
if (templateToMatch)
{
matchRank = false;
}
else
{ // No selection group name defined, so fall back to exact match
templateToMatch = Engine.GuiInterfaceCall("GetEntityState", selectedEntity).template;
}
}
// TODO: Should we handle "control all units" here as well?
ents = Engine.PickSimilarFriendlyEntities(templateToMatch, showOffscreen, matchRank);
}
else
{
// It's single click right now but it may become double or triple click
doubleClicked = false;
doubleClickTimer = now.getTime();
prevClickedEntity = selectedEntity;
// We only want to include the first picked unit in the selection
ents = [ents[0]];
}
// Update the list of selected units
if (Engine.HotkeyIsPressed("selection.add"))
{
g_Selection.addList(ents);
}
else if (Engine.HotkeyIsPressed("selection.remove"))
{
g_Selection.removeList(ents);
}
else
{
g_Selection.reset();
g_Selection.addList(ents);
}
inputState = INPUT_NORMAL;
return true;
}
break;
}
break;
case INPUT_BUILDING_PLACEMENT:
switch (ev.type)
{
case "mousemotion":
placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
var snapData = Engine.GuiInterfaceCall("GetFoundationSnapData", {
"template": placementEntity,
"x": placementPosition.x,
"z": placementPosition.z
});
if (snapData)
{
placementAngle = snapData.angle;
placementPosition.x = snapData.x;
placementPosition.z = snapData.z;
}
updateBuildingPlacementPreview();
return false; // continue processing mouse motion
case "mousebuttondown":
if (ev.button == SDL_BUTTON_LEFT)
{
placementPosition = Engine.GetTerrainAtPoint(ev.x, ev.y);
dragStart = [ ev.x, ev.y ];
inputState = INPUT_BUILDING_CLICK;
return true;
}
else if (ev.button == SDL_BUTTON_RIGHT)
{
// Cancel building
resetPlacementEntity();
inputState = INPUT_NORMAL;
return true;
}
break;
case "hotkeydown":
var rotation_step = Math.PI / 12; // 24 clicks make a full rotation
switch (ev.hotkey)
{
case "session.rotate.cw":
placementAngle += rotation_step;
updateBuildingPlacementPreview();
break;
case "session.rotate.ccw":
placementAngle -= rotation_step;
updateBuildingPlacementPreview();
break;
}
break;
}
break;
}
return false;
}
function doAction(action, ev)
{
var selection = g_Selection.toList();
// If shift is down, add the order to the unit's order queue instead
// of running it immediately
var queued = Engine.HotkeyIsPressed("session.queue");
switch (action.type)
{
case "move":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
return true;
case "attack":
Engine.PostNetworkCommand({"type": "attack", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_attack", "entity": selection[0] });
return true;
case "build": // (same command as repair)
case "repair":
Engine.PostNetworkCommand({"type": "repair", "entities": selection, "target": action.target, "autocontinue": true, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_repair", "entity": selection[0] });
return true;
case "gather":
Engine.PostNetworkCommand({"type": "gather", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
case "returnresource":
Engine.PostNetworkCommand({"type": "returnresource", "entities": selection, "target": action.target, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_gather", "entity": selection[0] });
return true;
case "garrison":
Engine.PostNetworkCommand({"type": "garrison", "entities": selection, "target": action.target, "queued": queued});
// TODO: Play a sound?
return true;
case "set-rallypoint":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z
});
return true;
case "unset-rallypoint":
var target = Engine.GetTerrainAtPoint(ev.x, ev.y);
Engine.PostNetworkCommand({"type": "unset-rallypoint", "entities": selection});
// Remove displayed rally point
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": []
});
return true;
case "none":
return true;
default:
error("Invalid action.type "+action.type);
return false;
}
}
function handleMinimapEvent(target)
{
// Partly duplicated from handleInputAfterGui(), but with the input being
// world coordinates instead of screen coordinates.
if (inputState == INPUT_NORMAL)
{
var fromMinimap = true;
var action = determineAction(undefined, undefined, fromMinimap);
if (!action)
return false;
var selection = g_Selection.toList();
var queued = Engine.HotkeyIsPressed("session.queue");
switch (action.type)
{
case "move":
Engine.PostNetworkCommand({"type": "walk", "entities": selection, "x": target.x, "z": target.z, "queued": queued});
Engine.GuiInterfaceCall("PlaySound", { "name": "order_walk", "entity": selection[0] });
return true;
case "set-rallypoint":
Engine.PostNetworkCommand({"type": "set-rallypoint", "entities": selection, "x": target.x, "z": target.z});
// Display rally point at the new coordinates, to avoid display lag
Engine.GuiInterfaceCall("DisplayRallyPoint", {
"entities": selection,
"x": target.x,
"z": target.z
});
return true;
default:
error("Invalid action.type "+action.type);
}
}
return false;
}
// Called by GUI when user clicks construction button
function startBuildingPlacement(buildEntType)
{
placementEntity = buildEntType;
placementAngle = defaultPlacementAngle;
inputState = INPUT_BUILDING_PLACEMENT;
}
+// Called by GUI when user clicks exchange resources button
+function exchangeResources(command)
+{
+ Engine.PostNetworkCommand({"type": "barter", "sell": command.sell, "buy": command.buy, "amount": command.amount});
+}
+
+
// Batch training:
// When the user shift-clicks, we set these variables and switch to INPUT_BATCHTRAINING
// When the user releases shift, or clicks on a different training button, we create the batched units
var batchTrainingEntity;
var batchTrainingType;
var batchTrainingCount;
const batchIncrementSize = 5;
function flushTrainingQueueBatch()
{
Engine.PostNetworkCommand({"type": "train", "entity": batchTrainingEntity, "template": batchTrainingType, "count": batchTrainingCount});
}
// Called by GUI when user clicks training button
function addToTrainingQueue(entity, trainEntType)
{
if (Engine.HotkeyIsPressed("session.batchtrain"))
{
if (inputState == INPUT_BATCHTRAINING)
{
// If we're already creating a batch of this unit, then just extend it
if (batchTrainingEntity == entity && batchTrainingType == trainEntType)
{
batchTrainingCount += batchIncrementSize;
return;
}
// Otherwise start a new one
else
{
flushTrainingQueueBatch();
// fall through to create the new batch
}
}
inputState = INPUT_BATCHTRAINING;
batchTrainingEntity = entity;
batchTrainingType = trainEntType;
batchTrainingCount = batchIncrementSize;
}
else
{
// Non-batched - just create a single entity
Engine.PostNetworkCommand({"type": "train", "entity": entity, "template": trainEntType, "count": 1});
}
}
// Returns the number of units that will be present in a batch if the user clicks
// the training button with shift down
function getTrainingQueueBatchStatus(entity, trainEntType)
{
if (inputState == INPUT_BATCHTRAINING && batchTrainingEntity == entity && batchTrainingType == trainEntType)
return [batchTrainingCount, batchIncrementSize];
else
return [0, batchIncrementSize];
}
// Called by GUI when user clicks production queue item
function removeFromTrainingQueue(entity, id)
{
Engine.PostNetworkCommand({"type": "stop-train", "entity": entity, "id": id});
}
// Called by unit selection buttons
function changePrimarySelectionGroup(templateName)
{
if (Engine.HotkeyIsPressed("session.deselectgroup"))
g_Selection.makePrimarySelection(templateName, true);
else
g_Selection.makePrimarySelection(templateName, false);
}
// Performs the specified command (delete, town bell, repair, etc.)
function performCommand(entity, commandName)
{
if (entity)
{
var entState = GetEntityState(entity);
var template = GetTemplateData(entState.template);
var unitName = getEntityName(template);
var playerID = Engine.GetPlayerID();
if (entState.player == playerID || g_DevSettings.controlAll)
{
switch (commandName)
{
case "delete":
var selection = g_Selection.toList();
if (selection.length > 0)
openDeleteDialog(selection);
break;
case "garrison":
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_GARRISON;
break;
case "repair":
inputState = INPUT_PRESELECTEDACTION;
preSelectedAction = ACTION_REPAIR;
break;
case "unload-all":
unloadAll(entity);
break;
default:
break;
}
}
}
}
// Performs the specified formation
function performFormation(entity, formationName)
{
if (entity)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "formation",
"entities": selection,
"name": formationName
});
}
}
// Performs the specified group
function performGroup(action, groupId)
{
switch (action)
{
case "snap":
case "select":
var toSelect = [];
g_Groups.update();
for (var ent in g_Groups.groups[groupId].ents)
toSelect.push(+ent);
g_Selection.reset();
g_Selection.addList(toSelect);
if (action == "snap" && toSelect.length)
Engine.CameraFollow(toSelect[0]);
break;
case "add":
var selection = g_Selection.toList();
g_Groups.addEntities(groupId, selection);
updateGroups();
break;
case "save":
var selection = g_Selection.toList();
g_Groups.groups[groupId].reset();
g_Groups.addEntities(groupId, selection);
updateGroups();
break;
}
}
// Performs the specified stance
function performStance(entity, stanceName)
{
if (entity)
{
var selection = g_Selection.toList();
Engine.PostNetworkCommand({
"type": "stance",
"entities": selection,
"name": stanceName
});
}
}
// Set the camera to follow the given unit
function setCameraFollow(entity)
{
// Follow the given entity if it's a unit
if (entity)
{
var entState = GetEntityState(entity);
if (entState && isUnit(entState))
{
Engine.CameraFollow(entity);
return;
}
}
// Otherwise stop following
Engine.CameraFollow(0);
}
var lastIdleUnit = 0;
var currIdleClass = 0;
function resetIdleUnit()
{
lastIdleUnit = 0;
currIdleClass = 0;
}
function findIdleUnit(classes)
{
// Cycle through idling classes before giving up
for (var i = 0; i <= classes.length; ++i)
{
var data = { prevUnit: lastIdleUnit, idleClass: classes[currIdleClass] };
var newIdleUnit = Engine.GuiInterfaceCall("FindIdleUnit", data);
// Check if we have new valid entity
if (newIdleUnit && newIdleUnit != lastIdleUnit)
{
lastIdleUnit = newIdleUnit;
g_Selection.reset()
g_Selection.addList([lastIdleUnit]);
Engine.CameraFollow(lastIdleUnit);
return;
}
lastIdleUnit = 0;
currIdleClass = (currIdleClass + 1) % classes.length;
}
// TODO: display a message or play a sound to indicate no more idle units, or something
// Reset for next cycle
resetIdleUnit();
}
function unload(garrisonHolder, entity)
{
Engine.PostNetworkCommand({"type": "unload", "entity": entity, "garrisonHolder": garrisonHolder});
}
function unloadAll(garrisonHolder)
{
Engine.PostNetworkCommand({"type": "unload-all", "garrisonHolder": garrisonHolder});
}
Index: ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 10587)
+++ ps/trunk/binaries/data/mods/public/gui/session/unit_commands.js (revision 10588)
@@ -1,471 +1,557 @@
// Panel types
const SELECTION = "Selection";
const QUEUE = "Queue";
const GARRISON = "Garrison";
const FORMATION = "Formation";
const TRAINING = "Training";
const CONSTRUCTION = "Construction";
const COMMAND = "Command";
const STANCE = "Stance";
// Constants
const COMMANDS_PANEL_WIDTH = 228;
const UNIT_PANEL_BASE = -52; // QUEUE: The offset above the main panel (will often be negative)
const UNIT_PANEL_HEIGHT = 44; // QUEUE: The height needed for a row of buttons
+// Barter constants
+const BARTER_RESOURCE_AMOUNT_TO_SELL = 100;
+const BARTER_BUNCH_MULTIPLIER = 5;
+const BARTER_RESOURCES = ["food", "wood", "stone", "metal"];
+const BARTER_ACTIONS = ["Sell", "Buy"];
+
// The number of currently visible buttons (used to optimise showing/hiding)
-var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Training": 0, "Construction": 0, "Command": 0, "Stance": 0};
+var g_unitPanelButtons = {"Selection": 0, "Queue": 0, "Formation": 0, "Garrison": 0, "Barter": 0, "Training": 0, "Construction": 0, "Command": 0, "Stance": 0};
// Unit panels are panels with row(s) of buttons
-var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Training", "Construction", "Research", "Stance", "Command"];
+var g_unitPanels = ["Selection", "Queue", "Formation", "Garrison", "Barter", "Training", "Construction", "Research", "Stance", "Command"];
+
+// Indexes of resources to sell and buy on barter panel
+var g_barterSell = 0;
+var g_barterBuy = 1;
// Lay out a row of centered buttons (does not work inside a loop like the other function)
function layoutButtonRowCentered(rowNumber, guiName, startIndex, endIndex, width)
{
var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom;
var buttonSpacer = buttonSideLength+1;
var colNumber = 0;
// Collect buttons
var buttons = [];
var icons = [];
for (var i = startIndex; i < endIndex; i++)
{
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]");
if (button)
{
buttons.push(button);
icons.push(icon);
}
}
// Location of middle button
var middleIndex = Math.ceil(buttons.length/2);
// Determine whether even or odd number of buttons
var center = (buttons.length/2 == Math.ceil(buttons.length/2))? Math.ceil(width/2) : Math.ceil(width/2+buttonSpacer/2);
// Left Side
for (var i = middleIndex-1; i >= 0; i--)
{
if (buttons[i])
{
var icon = icons[i];
var size = buttons[i].size;
size.left = center - buttonSpacer*colNumber - buttonSideLength;
size.right = center - buttonSpacer*colNumber;
size.top = buttonSpacer*rowNumber;
size.bottom = buttonSpacer*rowNumber + buttonSideLength;
buttons[i].size = size;
colNumber++;
}
}
// Right Side
center += 1; // add spacing to center buttons
colNumber = 0; // reset to 0
for (var i = middleIndex; i < buttons.length; i++)
{
if (buttons[i])
{
var icon = icons[i];
var size = buttons[i].size;
size.left = center + buttonSpacer*colNumber;
size.right = center + buttonSpacer*colNumber + buttonSideLength;
size.top = buttonSpacer*rowNumber;
size.bottom = buttonSpacer*rowNumber + buttonSideLength;
buttons[i].size = size;
colNumber++;
}
}
}
// Lay out button rows
function layoutButtonRow(rowNumber, guiName, buttonSideLength, buttonSpacer, startIndex, endIndex)
{
var colNumber = 0;
for (var i = startIndex; i < endIndex; i++)
{
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
if (button)
{
var size = button.size;
size.left = buttonSpacer*colNumber;
size.right = buttonSpacer*colNumber + buttonSideLength;
size.top = buttonSpacer*rowNumber;
size.bottom = buttonSpacer*rowNumber + buttonSideLength;
button.size = size;
colNumber++;
}
}
}
+function selectBarterResourceToSell(resourceIndex)
+{
+ g_barterSell = resourceIndex;
+ // g_barterBuy should be set to different value in case if it is the same as g_barterSell
+ // (it is no make sense to exchange resource to the same one).
+ // We change it cyclic to next value.
+ if (g_barterBuy == g_barterSell)
+ g_barterBuy = (g_barterBuy + 1) % BARTER_RESOURCES.length;
+}
+
// Sets up "unit panels" - the panels with rows of icons (Helper function for updateUnitDisplay)
function setupUnitPanel(guiName, usedPanels, unitEntState, items, callback)
{
usedPanels[guiName] = 1;
var numberOfItems = items.length;
var selection = g_Selection.toList();
var garrisonGroups = new EntityGroups();
// Determine how many buttons there should be
switch (guiName)
{
case SELECTION:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case QUEUE:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case GARRISON:
if (numberOfItems > 16)
numberOfItems = 16;
//Group garrisoned units based on class
garrisonGroups.add(unitEntState.garrisonHolder.entities);
break;
case STANCE:
if (numberOfItems > 5)
numberOfItems = 5;
case FORMATION:
if (numberOfItems > 16)
numberOfItems = 16;
break;
case TRAINING:
if (numberOfItems > 24)
numberOfItems = 24;
break;
case CONSTRUCTION:
if (numberOfItems > 24)
numberOfItems = 24;
break;
case COMMAND:
if (numberOfItems > 6)
numberOfItems = 6;
break;
default:
break;
}
// Make buttons
var i;
for (i = 0; i < numberOfItems; i++)
{
var item = items[i];
var entType = ((guiName == "Queue")? item.template : item);
var template;
if (guiName != "Formation" && guiName != "Command" && guiName != "Stance")
{
template = GetTemplateData(entType);
if (!template)
continue; // ignore attempts to use invalid templates (an error should have been reported already)
}
switch (guiName)
{
case SELECTION:
var name = getEntityName(template);
var tooltip = name;
var count = g_Selection.groups.getCount(item);
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : "");
break;
case QUEUE:
var tooltip = getEntityName(template);
var progress = Math.round(item.progress*100) + "%";
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (item.count > 1 ? item.count : "");
if (i == 0)
{
getGUIObjectByName("queueProgress").caption = (item.progress ? progress : "");
var size = getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size;
size.top = Math.round(item.progress*40);
getGUIObjectByName("unit"+guiName+"ProgressSlider["+i+"]").size = size;
}
break;
case GARRISON:
var name = getEntityName(template);
var tooltip = "Unload " + getEntityName(template);
var count = garrisonGroups.getCount(item);
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 1 ? count : "");
break;
case STANCE:
case FORMATION:
var tooltip = toTitleCase(item);
break;
case TRAINING:
var tooltip = getEntityNameWithGenericType(template);
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
var [batchSize, batchIncrement] = getTrainingQueueBatchStatus(unitEntState.id, entType);
var trainNum = batchSize ? batchSize+batchIncrement : batchIncrement;
tooltip += "\n" + getEntityCost(template);
if (template.health)
tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
if (template.armour)
tooltip += "\n[font=\"serif-bold-13\"]Armour:[/font] " + damageTypesToText(template.armour);
if (template.attack)
tooltip += "\n" + getEntityAttack(template);
if (template.speed)
tooltip += "\n" + getEntitySpeed(template);
tooltip += "\n\n[font=\"serif-bold-13\"]Shift-click[/font][font=\"serif-13\"] to train " + trainNum + ".[/font]";
break;
case CONSTRUCTION:
var tooltip = getEntityNameWithGenericType(template);
if (template.tooltip)
tooltip += "\n[font=\"serif-13\"]" + template.tooltip + "[/font]";
tooltip += "\n" + getEntityCost(template);
tooltip += getPopulationBonus(template);
if (template.health)
tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + template.health;
break;
case COMMAND:
if (item == "unload-all")
{
var count = unitEntState.garrisonHolder.entities.length;
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = (count > 0 ? count : "");
}
else
{
getGUIObjectByName("unit"+guiName+"Count["+i+"]").caption = "";
}
tooltip = toTitleCase(item);
break;
default:
break;
}
// Button
var button = getGUIObjectByName("unit"+guiName+"Button["+i+"]");
var icon = getGUIObjectByName("unit"+guiName+"Icon["+i+"]");
button.hidden = false;
button.tooltip = tooltip;
// Button Function (need nested functions to get the closure right)
button.onpress = (function(e){ return function() { callback(e) } })(item);
// Get icon image
if (guiName == "Formation")
{
icon.cell_id = getFormationCellId(item);
var formationOk = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
"ents": g_Selection.toList(),
"formationName": item
});
icon.enabled = formationOk;
button.enabled = formationOk;
if (!icon.enabled)
{
icon.sprite = "formation_disabled";
button.tooltip += " (disabled)";
}
else
{
icon.sprite = "formation";
}
}
else if (guiName == "Stance")
{
var stanceSelected = Engine.GuiInterfaceCall("IsStanceSelected", {
"ents": g_Selection.toList(),
"stance": item
});
icon.cell_id = i;
if (stanceSelected)
icon.sprite = "snIconSheetStanceButton";
else
icon.sprite = "snIconSheetStanceButtonDisabled";
}
else if (guiName == "Command")
{
//icon.cell_id = i;
//icon.cell_id = getCommandCellId(item);
icon.sprite = "stretched:session/icons/single/" + getCommandImage(item);
}
else if (template.icon)
{
icon.sprite = "stretched:session/portraits/" + template.icon;
}
else
{
// TODO: we should require all entities to have icons, so this case never occurs
icon.sprite = "bkFillBlack";
}
}
// Position the visible buttons (TODO: if there's lots, maybe they should be squeezed together to fit)
var numButtons = i;
var rowLength = 8;
if (guiName == "Selection")
rowLength = 4;
else if (guiName == "Formation" || guiName == "Garrison" || guiName == "Command")
rowLength = 4;
var numRows = Math.ceil(numButtons / rowLength);
var buttonSideLength = getGUIObjectByName("unit"+guiName+"Button[0]").size.bottom;
var buttonSpacer = buttonSideLength+1;
// Layout buttons
if (guiName == "Command")
{
layoutButtonRowCentered(0, guiName, 0, numButtons, COMMANDS_PANEL_WIDTH);
}
else
{
for (var i = 0; i < numRows; i++)
layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) );
}
// Resize Queue panel if needed
if (guiName == "Queue") // or garrison
{
var panel = getGUIObjectByName("unitQueuePanel");
var size = panel.size;
size.top = (UNIT_PANEL_BASE - ((numRows-1)*UNIT_PANEL_HEIGHT));
panel.size = size;
}
// Hide any buttons we're no longer using
for (i = numButtons; i < g_unitPanelButtons[guiName]; ++i)
getGUIObjectByName("unit"+guiName+"Button["+i+"]").hidden = true;
g_unitPanelButtons[guiName] = numButtons;
}
+// Sets up "unit barter panel" - special case for setupUnitPanel
+function setupUnitBarterPanel(unitEntState)
+{
+ // Amount of player's resource to exchange
+ var amountToSell = BARTER_RESOURCE_AMOUNT_TO_SELL;
+ if (Engine.HotkeyIsPressed("session.massbarter"))
+ amountToSell *= BARTER_BUNCH_MULTIPLIER;
+ // One pass for each resource
+ for (var i = 0; i < BARTER_RESOURCES.length; i++)
+ {
+ var resource = BARTER_RESOURCES[i];
+ // One pass for 'sell' row and another for 'buy'
+ for (var j = 0; j < 2; j++)
+ {
+ var selectedResourceIndex = [g_barterSell, g_barterBuy][j];
+ var action = BARTER_ACTIONS[j];
+
+ var imageNameSuffix = (i == selectedResourceIndex) ? "selected" : "inactive";
+ var icon = getGUIObjectByName("unitBarter" + action + "Icon["+i+"]");
+
+ var button = getGUIObjectByName("unitBarter" + action + "Button["+i+"]");
+ button.size = (i * 46) + " 0 " + ((i + 1) * 46) + " 46";
+ var amountToBuy;
+ // In 'buy' row show black icon in place corresponding to selected resource in 'sell' row
+ if (j == 1 && i == g_barterSell)
+ {
+ button.enabled = false;
+ button.tooltip = "";
+ icon.sprite = "";
+ amountToBuy = "";
+ }
+ else
+ {
+ button.enabled = true;
+ button.tooltip = action + " " + resource;
+ icon.sprite = "stretched:session/resources/" + resource + "_" + imageNameSuffix + ".png";
+ var sellPrice = unitEntState.barterMarket.prices["sell"][BARTER_RESOURCES[g_barterSell]];
+ var buyPrice = unitEntState.barterMarket.prices["buy"][resource];
+ amountToBuy = "+" + Math.round(sellPrice / buyPrice * amountToSell);
+ }
+ var amount;
+ if (j == 0)
+ {
+ button.onpress = (function(i){ return function() { selectBarterResourceToSell(i); } })(i);
+ amount = (i == g_barterSell) ? "-" + amountToSell : "";
+ }
+ else
+ {
+ button.onpress = (function(i){ return function() { g_barterBuy = i; } })(i);
+ amount = amountToBuy;
+ }
+ getGUIObjectByName("unitBarter" + action + "Amount["+i+"]").caption = amount;
+ }
+ }
+ var performDealButton = getGUIObjectByName("PerformDealButton");
+ var exchangeResourcesParameters = { "sell": BARTER_RESOURCES[g_barterSell], "buy": BARTER_RESOURCES[g_barterBuy], "amount": amountToSell };
+ performDealButton.onpress = function() { exchangeResources(exchangeResourcesParameters) };
+}
+
// Updates right Unit Commands Panel - runs in the main session loop via updateSelectionDetails()
function updateUnitCommands(entState, supplementalDetailsPanel, commandsPanel, selection)
{
//var isInvisible = true;
// Panels that are active
var usedPanels = {};
// If the selection is friendly units, add the command panels
var player = Engine.GetPlayerID();
if (entState.player == player || g_DevSettings.controlAll)
{
if (entState.attack) // TODO - this should be based on some AI properties
{
//usedPanels["Stance"] = 1;
//usedPanels["Formation"] = 1;
// (These are disabled since they're not implemented yet)
}
else // TODO - this should be based on various other things
{
//usedPanels["Research"] = 1;
}
if (selection.length > 1)
setupUnitPanel("Selection", usedPanels, entState, g_Selection.groups.getTemplateNames(),
function (entType) { changePrimarySelectionGroup(entType); } );
var commands = getEntityCommandsList(entState);
if (commands.length)
setupUnitPanel("Command", usedPanels, entState, commands,
function (item) { performCommand(entState.id, item); } );
if (entState.garrisonHolder)
{
var groups = new EntityGroups();
groups.add(entState.garrisonHolder.entities);
setupUnitPanel("Garrison", usedPanels, entState, groups.getTemplateNames(),
function (item) { unload(entState.id, groups.getEntsByName(item)[0]); } );
}
var formations = getEntityFormationsList(entState);
if (isUnit(entState) && !isAnimal(entState) && !entState.garrisonHolder && formations.length)
{
setupUnitPanel("Formation", usedPanels, entState, formations,
function (item) { performFormation(entState.id, item); } );
}
// TODO: probably should load the stance list from a data file,
// and/or vary depending on what units are selected
var stances = ["violent", "aggressive", "passive", "defensive", "stand"];
if (isUnit(entState) && !isAnimal(entState) && !entState.garrisonHolder && stances.length)
{
setupUnitPanel("Stance", usedPanels, entState, stances,
function (item) { performStance(entState.id, item); } );
}
+ getGUIObjectByName("unitBarterPanel").hidden = !entState.barterMarket;
+ if (entState.barterMarket)
+ {
+ usedPanels["Barter"] = 1;
+ setupUnitBarterPanel(entState);
+ }
+
if (entState.buildEntities && entState.buildEntities.length)
{
setupUnitPanel("Construction", usedPanels, entState, entState.buildEntities, startBuildingPlacement);
// isInvisible = false;
}
if (entState.training && entState.training.entities.length)
{
setupUnitPanel("Training", usedPanels, entState, entState.training.entities,
function (trainEntType) { addToTrainingQueue(entState.id, trainEntType); } );
// isInvisible = false;
}
if (entState.training && entState.training.queue.length)
setupUnitPanel("Queue", usedPanels, entState, entState.training.queue,
function (item) { removeFromTrainingQueue(entState.id, item.id); } );
// supplementalDetailsPanel.hidden = false;
// commandsPanel.hidden = isInvisible;
}
else
{
getGUIObjectByName("stamina").hidden = true;
// supplementalDetailsPanel.hidden = true;
// commandsPanel.hidden = true;
}
// Hides / unhides Unit Panels (panels should be grouped by type, not by order, but we will leave that for another time)
var offset = 0;
for each (var panelName in g_unitPanels)
{
var panel = getGUIObjectByName("unit" + panelName + "Panel");
if (usedPanels[panelName])
panel.hidden = false;
else
panel.hidden = true;
}
}
// Force hide commands panels
function hideUnitCommands()
{
for each (var panelName in g_unitPanels)
getGUIObjectByName("unit" + panelName + "Panel").hidden = true;
}
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 10587)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Commands.js (revision 10588)
@@ -1,677 +1,682 @@
// Setting this to true will display some warnings when commands
// are likely to fail, which may be useful for debugging AIs
var g_DebugCommands = false;
function ProcessCommand(player, cmd)
{
// Do some basic checks here that commanding player is valid
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerMan || player < 0)
return;
var playerEnt = cmpPlayerMan.GetPlayerByID(player);
if (playerEnt == INVALID_ENTITY)
return;
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
if (!cmpPlayer)
return;
var controlAllUnits = cmpPlayer.CanControlAllUnits();
// Note: checks of UnitAI targets are not robust enough here, as ownership
// can change after the order is issued, they should be checked by UnitAI
// when the specific behavior (e.g. attack, garrison) is performed.
// (Also it's not ideal if a command silently fails, it's nicer if UnitAI
// moves the entities closer to the target before giving up.)
// Now handle various commands
switch (cmd.type)
{
case "debug-print":
print(cmd.message);
break;
case "chat":
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": cmd.message});
break;
case "control-all":
cmpPlayer.SetControlAllUnits(cmd.flag);
break;
case "reveal-map":
// Reveal the map for all players, not just the current player,
// primarily to make it obvious to everyone that the player is cheating
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
break;
case "walk":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued);
});
break;
case "attack":
if (g_DebugCommands && !IsOwnedByEnemyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: attack target is not owned by enemy of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanAttack for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Attack(cmd.target, cmd.queued);
});
break;
case "repair":
// This covers both repairing damaged buildings, and constructing unfinished foundations
if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
}
// See UnitAI.CanRepair for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued);
});
break;
case "gather":
if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
{
// This check is for debugging only!
warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
}
// See UnitAI.CanGather for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Gather(cmd.target, cmd.queued);
});
break;
case "returnresource":
// Check dropsite is owned by player
if (g_DebugCommands && IsOwnedByPlayer(player, cmd.target))
{
// This check is for debugging only!
warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
}
// See UnitAI.CanReturnResource for target checks
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.ReturnResource(cmd.target, cmd.queued);
});
break;
case "train":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.AddBatch(cmd.template, +cmd.count, cmd.metadata);
}
else if (g_DebugCommands)
{
warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "stop-train":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.entity, player, controlAllUnits))
{
var queue = Engine.QueryInterface(cmd.entity, IID_TrainingQueue);
if (queue)
queue.RemoveBatch(cmd.id);
}
else if (g_DebugCommands)
{
warn("Invalid command: training building cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "construct":
// Message structure:
// {
// "type": "construct",
// "entities": [...],
// "template": "...",
// "x": ...,
// "z": ...,
// "angle": ...,
// "autorepair": true, // whether to automatically start constructing/repairing the new foundation
// "autocontinue": true, // whether to automatically gather/build/etc after finishing this
// "queued": true,
// }
/*
* Construction process:
* . Take resources away immediately.
* . Create a foundation entity with 1hp, 0% build progress.
* . Increase hp and build progress up to 100% when people work on it.
* . If it's destroyed, an appropriate fraction of the resource cost is refunded.
* . If it's completed, it gets replaced with the real building.
*/
// Check that we can control these units
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
if (!entities.length)
break;
// Tentatively create the foundation (we might find later that it's a invalid build command)
var ent = Engine.AddEntity("foundation|" + cmd.template);
if (ent == INVALID_ENTITY)
{
// Error (e.g. invalid template names)
error("Error creating foundation entity for '" + cmd.template + "'");
break;
}
// Move the foundation to the right place
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
cmpPosition.SetYRotation(cmd.angle);
// Check whether it's obstructed by other entities or invalid terrain
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions || !cmpBuildRestrictions.CheckPlacement(player))
{
if (g_DebugCommands)
{
warn("Invalid command: build restrictions check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was obstructed" });
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
break;
}
// Check build limits
var cmpBuildLimits = QueryPlayerIDInterface(player, IID_BuildLimits);
if (!cmpBuildLimits || !cmpBuildLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
{
if (g_DebugCommands)
{
warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
}
// TODO: The UI should tell the user they can't build this (but we still need this check)
// Remove the foundation because the construction was aborted
Engine.DestroyEntity(ent);
break;
}
// TODO: AI has no visibility info
if (!cmpPlayer.IsAI())
{
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
if (!visible)
{
if (g_DebugCommands)
{
warn("Invalid command: foundation visibility check failed for player "+player+": "+uneval(cmd));
}
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({ "player": player, "message": "Building site was not visible" });
Engine.DestroyEntity(ent);
break;
}
}
var cmpCost = Engine.QueryInterface(ent, IID_Cost);
if (!cmpPlayer.TrySubtractResources(cmpCost.GetResourceCosts()))
{
if (g_DebugCommands)
{
warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
}
Engine.DestroyEntity(ent);
break;
}
// Make it owned by the current player
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Initialise the foundation
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
cmpFoundation.InitialiseConstruction(player, cmd.template);
// Tell the units to start building this new entity
if (cmd.autorepair)
{
ProcessCommand(player, {
"type": "repair",
"entities": entities,
"target": ent,
"autocontinue": cmd.autocontinue,
"queued": cmd.queued
});
}
break;
case "delete-entities":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
for each (var ent in entities)
{
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
break;
case "set-rallypoint":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
for each (var ent in entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.SetPosition(cmd.x, cmd.z);
}
break;
case "unset-rallypoint":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
for each (var ent in entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
cmpRallyPoint.Unset();
}
break;
case "defeat-player":
// Send "OnPlayerDefeated" message to player
Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": player } );
break;
case "garrison":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.target, player, controlAllUnits))
{
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
cmpUnitAI.Garrison(cmd.target);
});
}
else if (g_DebugCommands)
{
warn("Invalid command: garrison target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "unload":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits))
{
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(cmd.entity))
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison unit"};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
}
else if (g_DebugCommands)
{
warn("Invalid command: unload target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "unload-all":
// Verify that the building can be controlled by the player
if (CanControlUnit(cmd.garrisonHolder, player, controlAllUnits))
{
var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
{
var cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
var notification = {"player": cmpPlayer.GetPlayerID(), "message": "Unable to ungarrison all units"};
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
}
else if (g_DebugCommands)
{
warn("Invalid command: unload-all target cannot be controlled by player "+player+": "+uneval(cmd));
}
break;
case "formation":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
GetFormationUnitAIs(entities).forEach(function(cmpUnitAI) {
var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation);
if (!cmpFormation)
return;
cmpFormation.LoadFormation(cmd.name);
cmpFormation.MoveMembersIntoFormation(true);
});
break;
case "promote":
// No need to do checks here since this is a cheat anyway
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGuiInterface.PushNotification({"type": "chat", "player": player, "message": "(Cheat - promoted units)"});
for each (var ent in cmd.entities)
{
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
}
break;
case "stance":
var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
for each (var ent in entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SwitchToStance(cmd.name);
}
break;
+ case "barter":
+ var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
+ cmpBarter.ExchangeResources(playerEnt, cmd.sell, cmd.buy, cmd.amount);
+ break;
+
default:
error("Invalid command: unknown command type: "+uneval(cmd));
}
}
/**
* Get some information about the formations used by entities.
* The entities must have a UnitAI component.
*/
function ExtractFormations(ents)
{
var entities = []; // subset of ents that have UnitAI
var members = {}; // { formationentity: [ent, ent, ...], ... }
for each (var ent in ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var fid = cmpUnitAI.GetFormationController();
if (fid != INVALID_ENTITY)
{
if (!members[fid])
members[fid] = [];
members[fid].push(ent);
}
entities.push(ent);
}
var ids = [ id for (id in members) ];
return { "entities": entities, "members": members, "ids": ids };
}
/**
* Remove the given list of entities from their current formations.
*/
function RemoveFromFormation(ents)
{
var formation = ExtractFormations(ents);
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
}
/**
* Returns a list of UnitAI components, each belonging either to a
* selected unit or to a formation entity for groups of the selected units.
*/
function GetFormationUnitAIs(ents)
{
// If an individual was selected, remove it from any formation
// and command it individually
if (ents.length == 1)
{
// Skip unit if it has no UnitAI
var cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
if (!cmpUnitAI)
return [];
RemoveFromFormation(ents);
return [ cmpUnitAI ];
}
// Separate out the units that don't support the chosen formation
var formedEnts = [];
var nonformedUnitAIs = [];
for each (var ent in ents)
{
// Skip units with no UnitAI
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI)
continue;
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: Currently we use LineClosed as effectively a boolean flag
// to determine whether formations are allowed at all. Instead we
// should check specific formation names and do something sensible
// (like what?) when some units don't support them.
// TODO: We'll also need to fix other formation code to use
// "LineClosed" instead of "Line Closed" etc consistently.
if (cmpIdentity && cmpIdentity.CanUseFormation("LineClosed"))
formedEnts.push(ent);
else
nonformedUnitAIs.push(cmpUnitAI);
}
if (formedEnts.length == 0)
{
// No units support the foundation - return all the others
return nonformedUnitAIs;
}
// Find what formations the formationable selected entities are currently in
var formation = ExtractFormations(formedEnts);
var formationEnt = undefined;
if (formation.ids.length == 1)
{
// Selected units all belong to the same formation.
// Check that it doesn't have any other members
var fid = formation.ids[0];
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation && cmpFormation.GetMemberCount() == formation.entities.length)
{
// The whole formation was selected, so reuse its controller for this command
formationEnt = +fid;
}
}
if (!formationEnt)
{
// We need to give the selected units a new formation controller
// Remove selected units from their current formation
for (var fid in formation.members)
{
var cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
if (cmpFormation)
cmpFormation.RemoveMembers(formation.members[fid]);
}
// Create the new controller
formationEnt = Engine.AddEntity("special/formation");
var cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
cmpFormation.SetMembers(formation.entities);
// If all the selected units were previously in formations of the same shape,
// then set this new formation to that shape too; otherwise use the default shape
var lastFormationName = undefined;
for each (var ent in formation.entities)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
var name = cmpUnitAI.GetLastFormationName();
if (lastFormationName === undefined)
{
lastFormationName = name;
}
else if (lastFormationName != name)
{
lastFormationName = undefined;
break;
}
}
}
var formationName;
if (lastFormationName)
formationName = lastFormationName;
else
formationName = "Line Closed";
if (CanMoveEntsIntoFormation(formation.entities, formationName))
{
cmpFormation.LoadFormation(formationName);
}
else
{
cmpFormation.LoadFormation("Loose");
}
}
return nonformedUnitAIs.concat(Engine.QueryInterface(formationEnt, IID_UnitAI));
}
function CanMoveEntsIntoFormation(ents, formationName)
{
var count = ents.length;
var classesRequired;
// TODO: should check the player's civ is allowed to use this formation
if (formationName == "Loose")
{
return true;
}
else if (formationName == "Box")
{
if (count < 4)
return false;
}
else if (formationName == "Column Closed")
{
}
else if (formationName == "Line Closed")
{
}
else if (formationName == "Column Open")
{
}
else if (formationName == "Line Open")
{
}
else if (formationName == "Flank")
{
if (count < 8)
return false;
}
else if (formationName == "Skirmish")
{
classesRequired = ["Ranged"];
}
else if (formationName == "Wedge")
{
if (count < 3)
return false;
classesRequired = ["Cavalry"];
}
else if (formationName == "Formation12")
{
}
else if (formationName == "Phalanx")
{
if (count < 10)
return false;
classesRequired = ["Melee", "Infantry"];
}
else if (formationName == "Syntagma")
{
if (count < 9)
return false;
classesRequired = ["Melee", "Infantry"]; // TODO: pike only
}
else if (formationName == "Testudo")
{
if (count < 9)
return false;
classesRequired = ["Melee", "Infantry"];
}
else
{
return false;
}
var looseOnlyUnits = true;
for each (var ent in ents)
{
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
{
var classes = cmpIdentity.GetClassesList();
if (looseOnlyUnits && (classes.indexOf("Worker") == -1 || classes.indexOf("Support") == -1))
looseOnlyUnits = false;
for each (var classRequired in classesRequired)
{
if (classes.indexOf(classRequired) == -1)
{
return false;
}
}
}
}
if (looseOnlyUnits)
return false;
return true;
}
/**
* Check if player can control this entity
* returns: true if the entity is valid and owned by the player if
* or control all units is activated for the player, else false
*/
function CanControlUnit(entity, player, controlAll)
{
return (IsOwnedByPlayer(player, entity) || controlAll);
}
/**
* Filter entities which the player can control
*/
function FilterEntityList(entities, player, controlAll)
{
return entities.filter(function(ent) { return CanControlUnit(ent, player, controlAll);} );
}
Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 10587)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 10588)
@@ -1,743 +1,749 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised so we mustn't serialise
// its non-deterministic data. Instead just return an empty object.
return {};
};
GuiInterface.prototype.Deserialize = function(obj)
{
this.Init();
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.rallyPoints = undefined;
this.notifications = [];
this.renamedEntities = [];
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function(player)
{
var ret = {
"players": []
};
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits);
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// store player ally/enemy data as arrays
var allies = [];
var enemies = [];
for (var j = 0; j <= n; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
var playerData = {
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"colour": cmpPlayer.GetColour(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"phase": cmpPlayer.GetPhase(),
"isAlly": allies,
"isEnemy": enemies,
"buildLimits": cmpPlayerBuildLimits.GetLimits(),
"buildCounts": cmpPlayerBuildLimits.GetCounts()
};
ret.players.push(playerData);
}
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
{
ret.circularMap = cmpRangeManager.GetLosCircular();
}
// Add timeElapsed
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
return ret;
};
GuiInterface.prototype.GetExtendedSimulationState = function(player)
{
// Get basic simulation info
var ret = this.GetSimulationState();
// Add statistics to each player
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var n = cmpPlayerMan.GetNumPlayers();
for (var i = 0; i < n; ++i)
{
var playerEnt = cmpPlayerMan.GetPlayerByID(i);
var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function(player)
{
this.renamedEntities = [];
};
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTempMan.GetCurrentTemplateName(ent);
if (!template)
return null;
var ret = {
"id": ent,
"template": template
}
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
{
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName()
};
}
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
}
var cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
}
var cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show?
ret.attack = cmpAttack.GetAttackStrengths(type);
}
var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
{
ret.armour = cmpArmour.GetArmourStrengths();
}
var cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
{
ret.buildEntities = cmpBuilder.GetEntitiesList();
}
var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue);
if (cmpTrainingQueue)
{
ret.training = {
"entities": cmpTrainingQueue.GetEntitiesList(),
"queue": cmpTrainingQueue.GetQueue(),
};
}
var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
{
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage()
};
}
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
{
ret.player = cmpOwnership.GetOwner();
}
var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
{
ret.resourceSupply = {
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType()
};
}
var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
}
var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
{
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes()
};
}
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
{
ret.rallyPoint = { };
}
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
{
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClassesList()
};
}
var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
{
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
}
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
ret.unitAI = {
// TODO: reading properties directly is kind of violating abstraction
"state": cmpUnitAI.fsmStateName,
"orders": cmpUnitAI.orderQueue,
};
}
+ if (!cmpFoundation && cmpIdentity.HasClass("BarterMarket"))
+ {
+ var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
+ ret.barterMarket = { "prices": cmpBarter.GetPrices() };
+ }
+
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false);
return ret;
};
GuiInterface.prototype.GetTemplateData = function(player, name)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(name);
if (!template)
return null;
var ret = {};
if (template.Armour)
{
ret.armour = {
"hack": +template.Armour.Hack,
"pierce": +template.Armour.Pierce,
"crush": +template.Armour.Crush,
};
}
if (template.Attack)
{
ret.attack = {};
for (var type in template.Attack)
{
ret.attack[type] = {
"hack": (+template.Attack[type].Hack || 0),
"pierce": (+template.Attack[type].Pierce || 0),
"crush": (+template.Attack[type].Crush || 0),
};
}
}
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
if (template.Cost.Population) ret.cost.population = +template.Cost.Population;
if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
}
if (template.Health)
{
ret.health = +template.Health.Max;
}
if (template.Identity)
{
ret.selectionGroupName = template.Identity.SelectionGroupName;
ret.name = {
"specific": (template.Identity.SpecificName || template.Identity.GenericName),
"generic": template.Identity.GenericName
};
ret.icon = template.Identity.Icon;
ret.tooltip = template.Identity.Tooltip;
}
if (template.UnitMotion)
{
ret.speed = {
"walk": +template.UnitMotion.WalkSpeed,
};
if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed;
}
return ret;
};
GuiInterface.prototype.PushNotification = function(notification)
{
this.notifications.push(notification);
};
GuiInterface.prototype.GetNextNotification = function()
{
if (this.notifications.length)
return this.notifications.pop();
else
return "";
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationName);
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (var ent in data.ents)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
if (cmpUnitAI.GetStanceName() == data.stance)
return true;
}
}
return false;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
var playerColours = {}; // cache of owner -> colour map
for each (var ent in cmd.entities)
{
var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
if (cmd.alpha == 0)
{
cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0});
continue;
}
// Find the entity's owner's colour:
var owner = -1;
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
var colour = playerColours[owner];
if (!colour)
{
colour = {"r":1, "g":1, "b":1};
var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
if (cmpPlayer)
colour = cmpPlayer.GetColour();
playerColours[owner] = colour;
}
cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha});
}
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
for each (var ent in cmd.entities)
{
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.SetEnabled(cmd.enabled);
}
};
/**
* Displays the rally point of a building
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
// If there are rally points already displayed, destroy them
for each (var ent in this.rallyPoints)
{
// Hide it first (the destruction won't be instantaneous)
var cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.MoveOutOfWorld();
Engine.DestroyEntity(ent);
}
this.rallyPoints = [];
var positions = [];
// DisplayRallyPoints is called passing a list of entities for which
// rally points must be displayed
for each (var ent in cmd.entities)
{
var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position
var pos;
if (cmd.x && cmd.z)
pos = {"x": cmd.x, "z": cmd.z};
else
pos = cmpRallyPoint.GetPosition();
if (pos)
{
// TODO: it'd probably be nice if we could draw some kind of line
// between the building and pos, to make the marker easy to find even
// if it's a long way from the building
positions.push(pos);
}
}
// Add rally point entity for each building
for each (var pos in positions)
{
var rallyPoint = Engine.AddLocalEntity("actor|props/special/common/waypoint_flag.xml");
var cmpPosition = Engine.QueryInterface(rallyPoint, IID_Position);
cmpPosition.JumpTo(pos.x, pos.z);
this.rallyPoints.push(rallyPoint);
}
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
* Returns true if the placement is okay (everything is valid and the entity is not obstructed by others).
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
// See if we're changing template
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
// Destroy the old preview if there was one
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
// Load the new template
if (cmd.template == "")
{
this.placementEntity = undefined;
}
else
{
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
}
if (this.placementEntity)
{
var ent = this.placementEntity[1];
// Move the preview into the right location
var pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
// Check whether it's in a visible or fogged region
// tell GetLosVisibility to force RetainInFog because preview entities set this to false,
// which would show them as hidden instead of fogged
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden");
var validPlacement = false;
if (visible)
{ // Check whether it's obstructed by other entities or invalid terrain
var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player));
}
var ok = (visible && validPlacement);
// Set it to a red shade if this is an invalid location
var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!ok)
cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColour(1, 1, 1, 1);
}
return ok;
}
return false;
};
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateMgr.GetTemplate(data.template);
if (template.BuildRestrictions.Category == "Dock")
{
var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
if (!cmpTerrain || !cmpWaterManager)
{
return false;
}
// Get footprint size
var halfSize = 0;
if (template.Footprint.Square)
{
halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
}
else if (template.Footprint.Circle)
{
halfSize = template.Footprint.Circle["@radius"];
}
/* Find direction of most open water, algorithm:
* 1. Pick points in a circle around dock
* 2. If point is in water, add to array
* 3. Scan array looking for consecutive points
* 4. Find longest sequence of consecutive points
* 5. If sequence equals all points, no direction can be determined,
* expand search outward and try (1) again
* 6. Calculate angle using average of sequence
*/
const numPoints = 16;
for (var dist = 0; dist < 4; ++dist)
{
var waterPoints = [];
for (var i = 0; i < numPoints; ++i)
{
var angle = (i/numPoints)*2*Math.PI;
var d = halfSize*(dist+1);
var nx = data.x - d*Math.sin(angle);
var nz = data.z + d*Math.cos(angle);
if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
{
waterPoints.push(i);
}
}
var consec = [];
var length = waterPoints.length;
for (var i = 0; i < length; ++i)
{
var count = 0;
for (var j = 0; j < (length-1); ++j)
{
if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length])
{
++count;
}
else
{
break;
}
}
consec[i] = count;
}
var start = 0;
var count = 0;
for (var c in consec)
{
if (consec[c] > count)
{
start = c;
count = consec[c];
}
}
// If we've found a shoreline, stop searching
if (count != numPoints-1)
{
return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)};
}
}
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
// Ignore if no entity was passed
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
function isIdleUnit(ent, idleClass)
{
var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
// TODO: Do something with garrisoned idle units
return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass));
}
GuiInterface.prototype.FindIdleUnit = function(player, data)
{
var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var playerEntities = rangeMan.GetEntitiesByPlayer(player);
// Find the first matching entity that is after the previous selection,
// so that we cycle around in a predictable order
for each (var ent in playerEntities)
{
if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass))
return ent;
}
// No idle entities left in the class
return 0;
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder);
cmpPathfinder.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
cmpObstructionManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for each (var ent in data.entities)
{
var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetDebugOverlay(enabled);
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
}
// List the GuiInterface functions that can be safely called by GUI scripts.
// (GUI scripts are non-deterministic and untrusted, so these functions must be
// appropriately careful. They are called with a first argument "player", which is
// trusted and indicates the player associated with the current client; no data should
// be returned unless this player is meant to be able to see it.)
var exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetTemplateData": 1,
"GetNextNotification": 1,
"CanMoveEntsIntoFormation": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"SetStatusBars": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnit": 1,
"SetPathfinderDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
else
throw new Error("Invalid GuiInterface Call name \""+name+"\"");
};
Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Barter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Barter.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/Barter.js (revision 10588)
@@ -0,0 +1 @@
+Engine.RegisterInterface("Barter");
Index: ps/trunk/binaries/data/mods/public/simulation/components/Barter.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Barter.js (revision 10588)
@@ -0,0 +1,139 @@
+// True price of 100 units of resource (for case if some resource is more worth).
+// With current bartering system only relative values makes sense
+// so if for example stone is two times more expensive than wood,
+// there will 2:1 exchange rate.
+const TRUE_PRICES = { "food": 100, "wood": 100, "stone": 100, "metal": 100 };
+
+// Constant part of price difference between true price and buy/sell price.
+// In percents.
+// Buy price equal to true price plus constant difference.
+// Sell price equal to true price minus constant difference.
+const CONSTANT_DIFFERENCE = 10;
+
+// Additional difference of prices, added after each deal to specified resource price.
+// In percents.
+const DIFFERENCE_PER_DEAL = 5;
+
+// Price difference which restored each restore timer tick
+// In percents.
+const DIFFERENCE_RESTORE = 2;
+
+// Interval of timer which slowly restore prices after deals
+const RESTORE_TIMER_INTERVAL = 5000;
+
+// Array of resource names
+const RESOURCES = ["food", "wood", "stone", "metal"];
+
+function Barter() {}
+
+Barter.prototype.Schema =
+ "";
+
+Barter.prototype.Init = function()
+{
+ this.priceDifferences = {};
+ for each (var resource in RESOURCES)
+ this.priceDifferences[resource] = 0;
+ this.restoreTimer = undefined;
+};
+
+Barter.prototype.GetPrices = function()
+{
+ var prices = { "buy": {}, "sell": {} };
+ for each (var resource in RESOURCES)
+ {
+ prices["buy"][resource] = TRUE_PRICES[resource] * (100 + CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
+ prices["sell"][resource] = TRUE_PRICES[resource] * (100 - CONSTANT_DIFFERENCE + this.priceDifferences[resource]) / 100;
+ }
+ return prices;
+};
+
+Barter.prototype.PlayerHasMarket = function(playerEntity)
+{
+ var cmpPlayer = Engine.QueryInterface(playerEntity, IID_Player);
+ var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
+ var entities = cmpRangeManager.GetEntitiesByPlayer(cmpPlayer.GetPlayerID());
+ for each (var entity in entities)
+ {
+ var cmpFoundation = Engine.QueryInterface(entity, IID_Foundation);
+ var cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
+ if (!cmpFoundation && cmpIdentity.HasClass("BarterMarket"))
+ return true;
+ }
+ return false;
+}
+
+Barter.prototype.ExchangeResources = function(playerEntity, resourceToSell, resourceToBuy, amount)
+{
+ // Data verification
+ if (amount <= 0)
+ {
+ warn("ExchangeResources: incorrect amount: " + uneval(amount));
+ return;
+ }
+ if (RESOURCES.indexOf(resourceToSell) == -1)
+ {
+ warn("ExchangeResources: incorrect resource to sell: " + uneval(resourceToSell));
+ return;
+ }
+ if (RESOURCES.indexOf(resourceToBuy) == -1)
+ {
+ warn("ExchangeResources: incorrect resource to buy: " + uneval(resourceToBuy));
+ return;
+ }
+ if (!this.PlayerHasMarket(playerEntity))
+ {
+ warn("ExchangeResources: player has no markets");
+ return;
+ }
+
+ var cmpPlayer = Engine.QueryInterface(playerEntity, IID_Player);
+ var prices = this.GetPrices();
+ var amountsToSubtract = {};
+ amountsToSubtract[resourceToSell] = amount;
+ if (cmpPlayer.TrySubtractResources(amountsToSubtract))
+ {
+ var amountToAdd = Math.round(prices["sell"][resourceToSell] / prices["buy"][resourceToBuy] * amount);
+ cmpPlayer.AddResource(resourceToBuy, amountToAdd);
+ var numberOfDeals = Math.round(amount / 100);
+
+ // Increase price difference for both exchange resources.
+ // Overal price difference (constant + dynamic) can't exceed +-99%
+ // so both buy/sell prices limited to [1%; 199%] interval.
+ this.priceDifferences[resourceToSell] -= DIFFERENCE_PER_DEAL * numberOfDeals;
+ this.priceDifferences[resourceToSell] = Math.min(99-CONSTANT_DIFFERENCE, Math.max(CONSTANT_DIFFERENCE-99, this.priceDifferences[resourceToSell]));
+ this.priceDifferences[resourceToBuy] += DIFFERENCE_PER_DEAL * numberOfDeals;
+ this.priceDifferences[resourceToBuy] = Math.min(99-CONSTANT_DIFFERENCE, Math.max(CONSTANT_DIFFERENCE-99, this.priceDifferences[resourceToBuy]));
+ }
+
+ if (this.restoreTimer == undefined)
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ this.restoreTimer = cmpTimer.SetInterval(this.entity, IID_Barter, "ProgressTimeout", RESTORE_TIMER_INTERVAL, RESTORE_TIMER_INTERVAL, {});
+ }
+};
+
+Barter.prototype.ProgressTimeout = function(data)
+{
+ var needRestore = false;
+ for each (var resource in RESOURCES)
+ {
+ // Calculate value to restore, it should be limited to [-DIFFERENCE_RESTORE; DIFFERENCE_RESTORE] interval
+ var differenceRestore = Math.min(DIFFERENCE_RESTORE, Math.max(-DIFFERENCE_RESTORE, this.priceDifferences[resource]));
+ differenceRestore = -differenceRestore;
+ this.priceDifferences[resource] += differenceRestore;
+ // If price difference still exists then set flag to run timer again
+ if (this.priceDifferences[resource] != 0)
+ needRestore = true;
+ }
+
+ if (!needRestore)
+ {
+ var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
+ cmpTimer.CancelTimer(this.restoreTimer);
+ this.restoreTimer = undefined;
+ }
+}
+
+Engine.RegisterComponentType(IID_Barter, "Barter", Barter);
+
Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 10587)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 10588)
@@ -1,198 +1,199 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the unit type, typically for GUI display to users." +
"" +
"hele" +
"Infantry Spearman" +
"Hoplite" +
"units/hele_infantry_spearman.png" +
"" +
"" +
"" +
"gaia" +
"cart" +
"celt" +
"hele" +
"iber" +
"pers" +
"rome" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"Unit" +
"Infantry" +
"Cavalry" +
"Ranged" +
"Melee" +
"Mechanical" +
"Ship" +
"Siege" +
"Super" +
"Hero" +
"Support" +
"Animal" +
"Organic" +
"Structure" +
"Civic" +
"CivCentre" +
"Economic" +
"Defensive" +
+ "BarterMarket" +
"Village" +
"Town" +
"City" +
"ConquestCritical" +
"Worker" +
"CitizenSoldier" +
"Trade" +
"Warship" +
"SeaCreature" +
"ForestPlant" +
"DropsiteFood" +
"DropsiteWood" +
"DropsiteStone" +
"DropsiteMetal" +
"Bow" + // TODO: what are these used for?
"Javelin" +
"Spear" +
"Sword" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"Loose" +
"Box" +
"ColumnClosed" +
"LineClosed" +
"ColumnOpen" +
"LineOpen" +
"Flank" +
"Skirmish" +
"Wedge" +
"Testudo" +
"Phalanx" +
"Syntagma" +
"Formation12" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
};
Identity.prototype.Serialize = null; // we have no dynamic state to save
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetRank = function()
{
return (this.template.Rank || "");
};
Identity.prototype.GetClassesList = function()
{
if (this.template.Classes && "_string" in this.template.Classes)
{
var string = this.template.Classes._string;
return string.split(/\s+/);
}
else
{
return [];
}
};
Identity.prototype.HasClass = function(name)
{
return this.GetClassesList().indexOf(name) != -1;
};
Identity.prototype.GetFormationsList = function()
{
if (this.template.Formations && "_string" in this.template.Formations)
{
var string = this.template.Formations._string;
return string.split(/\s+/);
}
else
{
return [];
}
};
Identity.prototype.CanUseFormation = function(name)
{
return this.GetFormationsList().indexOf(name) != -1;
};
Identity.prototype.GetSelectionGroupName = function()
{
return (this.template.SelectionGroupName || "");
}
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 10587)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_structure_economic_market.xml (revision 10588)
@@ -1,56 +1,56 @@
10.040.020.0Market1503008.01500Market
- Create Trade units and Barter resources. (Currently a useless structure)
- Town
+ Create Trade units and Barter resources.
+ Town BarterMarketstructures/market.pnginterface/complete/building/complete_market.xmlattack/destruction/building_collapse_large.xmlfalse4065536
units/{civ}_support_trader
32structures/fndn_4x4.xml
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_selected.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_selected.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_selected.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_selected.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_selected.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_selected.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_selected.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_selected.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_selected.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_selected.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_selected.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_selected.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_selected.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_selected.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_selected.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_inactive.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_inactive.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_inactive.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_inactive.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_inactive.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_inactive.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_inactive.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_inactive.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_inactive.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/food_inactive.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_inactive.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_inactive.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_inactive.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_inactive.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/wood_inactive.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_inactive.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_inactive.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_inactive.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_inactive.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/stone_inactive.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_selected.png
===================================================================
Cannot display: file marked as a binary type.
svn:mime-type = application/octet-stream
Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_selected.png
===================================================================
--- ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_selected.png (nonexistent)
+++ ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_selected.png (revision 10588)
Property changes on: ps/trunk/binaries/data/mods/public/art/textures/ui/session/resources/metal_selected.png
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/octet-stream
\ No newline at end of property
Index: ps/trunk/binaries/data/config/default.cfg
===================================================================
--- ps/trunk/binaries/data/config/default.cfg (revision 10587)
+++ ps/trunk/binaries/data/config/default.cfg (revision 10588)
@@ -1,227 +1,228 @@
; 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 = false
; 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
shadowpcf = true
vsync = false
nos3tc = false
noautomipmap = true
novbo = false
noframebufferobject = false
; Linux only: Set the driconf force_s3tc_enable option at startup,
; for compressed texture support
force_s3tc_enable = true
; 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
; shader Use vertex/fragment shaders for transform and lighting where possible
; Using 'fixed' instead of 'default' may work around some graphics-related problems,
; but will reduce performance and features 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
; Opt-in online user reporting system
userreport.url = "http://feedback.wildfiregames.com/report/upload/v1/"
; Font mappings:
font.console = console
font.default = palatino12
font.misc = verdana16
; Colour of the sky (in "r g b" format)
skycolor = "0 0 0"
; GENERAL PREFERENCES:
sound.mastergain = 0.5
; Camera control settings
view.scroll.speed = 120.0
view.rotate.x.speed = 1.2
view.rotate.x.min = 28.0
view.rotate.x.max = 60.0
view.rotate.x.default = 35.0
view.rotate.y.speed = 2.0
view.rotate.y.speed.wheel = 0.45
view.rotate.y.default = 0.0
view.drag.speed = 0.5
view.zoom.speed = 256.0
view.zoom.speed.wheel = 32.0
view.zoom.min = 50.0
view.zoom.max = 200.0
view.zoom.default = 100.0
view.pos.smoothness = 0.1
view.zoom.smoothness = 0.4
view.rotate.x.smoothness = 0.5
view.rotate.y.smoothness = 0.3
view.near = 2.0 ; Near plane distance
view.far = 4096.0 ; Far plane distance
view.fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide
; 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 = "Alt+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 = "Shift+F2" ; Take large BMP screenshot
hotkey.togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode
hotkey.screenshot.watermark = "K" ; Toggle product/company watermark for official screenshots
hotkey.wireframe = "Alt+W" ; Toggle wireframe mode
; > CAMERA SETTINGS
hotkey.camera.reset = "H" ; Reset camera rotation to default.
hotkey.camera.follow = "F" ; Follow the first unit in the selection
hotkey.camera.zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control)
hotkey.camera.zoom.out = Minus, NumMinus ; Zoom camera out (continuous control)
hotkey.camera.zoom.wheel.in = WheelUp ; Zoom camera in (stepped control)
hotkey.camera.zoom.wheel.out = WheelDown ; Zoom camera out (stepped control)
hotkey.camera.rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards
hotkey.camera.rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards
hotkey.camera.rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain
hotkey.camera.rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain
hotkey.camera.rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control)
hotkey.camera.rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control)
hotkey.camera.pan = MouseMiddle, ForwardSlash ; Enable scrolling by moving mouse
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
; > CONSOLE SETTINGS
hotkey.console.toggle = BackQuote, F9 ; Open/close console
; > CLIPBOARD CONTROLS
hotkey.copy = "Ctrl+C" ; Copy to clipboard
hotkey.paste = "Ctrl+V" ; Paste from clipboard
hotkey.cut = "Ctrl+X" ; Cut selected text and copy to the clipboard
; > ENTITY SELECTION
hotkey.selection.add = Shift ; Add units to selection
hotkey.selection.remove = Ctrl ; Remove units from selection
hotkey.selection.idleworker = Period ; Select next idle worker
hotkey.selection.idlewarrior = Comma ; Select next idle warrior
hotkey.selection.offscreen = Alt ; Include offscreen units in selection
hotkey.selection.group.select.0 = 0
hotkey.selection.group.save.0 = "Ctrl+0"
hotkey.selection.group.add.0 = "Shift+0"
hotkey.selection.group.select.1 = 1
hotkey.selection.group.save.1 = "Ctrl+1"
hotkey.selection.group.add.1 = "Shift+1"
hotkey.selection.group.select.2 = 2
hotkey.selection.group.save.2 = "Ctrl+2"
hotkey.selection.group.add.2 = "Shift+2"
hotkey.selection.group.select.3 = 3
hotkey.selection.group.save.3 = "Ctrl+3"
hotkey.selection.group.add.3 = "Shift+3"
hotkey.selection.group.select.4 = 4
hotkey.selection.group.save.4 = "Ctrl+4"
hotkey.selection.group.add.4 = "Shift+4"
hotkey.selection.group.select.5 = 5
hotkey.selection.group.save.5 = "Ctrl+5"
hotkey.selection.group.add.5 = "Shift+5"
hotkey.selection.group.select.6 = 6
hotkey.selection.group.save.6 = "Ctrl+6"
hotkey.selection.group.add.6 = "Shift+6"
hotkey.selection.group.select.7 = 7
hotkey.selection.group.save.7 = "Ctrl+7"
hotkey.selection.group.add.7 = "Shift+7"
hotkey.selection.group.select.8 = 8
hotkey.selection.group.save.8 = "Ctrl+8"
hotkey.selection.group.add.8 = "Shift+8"
hotkey.selection.group.select.9 = 9
hotkey.selection.group.save.9 = "Ctrl+9"
hotkey.selection.group.add.9 = "Shift+9"
; > SESSION CONTROLS
hotkey.session.kill = Delete ; Destroy selected units
hotkey.session.garrison = Ctrl ; Modifier to garrison when clicking on building
hotkey.session.queue = Shift ; Modifier to queue unit orders instead of replacing
hotkey.session.batchtrain = Shift ; Modifier to train units in batches
+hotkey.session.massbarter = Shift ; Modifier to barter bunch of resources
hotkey.session.deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting
hotkey.session.rotate.cw = RightBracket ; Rotate building placement preview clockwise
hotkey.session.rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise
hotkey.timewarp.fastforward = Space ; If timewarp mode enabled, speed up the game
hotkey.timewarp.rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game
; > OVERLAY KEYS
hotkey.fps.toggle = "Shift+F" ; Toggle frame counter
hotkey.session.devcommands.toggle = "Shift+D" ; Toggle developer commands panel
hotkey.session.gui.toggle = "G" ; Toggle visibility of session GUI
hotkey.menu.toggle = "F10" ; Toggle in-game menu
hotkey.timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter
; > HOTKEYS ONLY
hotkey.chat = Return ; Toggle chat window
; > GUI TEXTBOX HOTKEYS
hotkey.text.delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor
hotkey.text.delete.right = "Ctrl+Del" ; Delete word to the right of cursor
hotkey.text.move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor
hotkey.text.move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor
; > PROFILER
hotkey.profile.toggle = "F11" ; Enable/disable real-time profiler
hotkey.profile.save = "Shift+F11" ; Save current profiler data to logs/profile.txt
hotkey.profile2.enable = "F11" ; Enable HTTP/GPU modes for new profiler
profiler2.http.autoenable = false ; Enable HTTP server output at startup (default off for security/performance)
profiler2.gpu.autoenable = false ; Enable GPU timing at startup (default off for performance/compatibility)
profiler2.gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available
profiler2.gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available
profiler2.gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available
; > QUICKSAVE
hotkey.quicksave = "Shift+F5"
hotkey.quickload = "Shift+F8"
; EXPERIMENTAL: joystick/gamepad settings
joystick.enable = false
joystick.deadzone = 8192
joystick.camera.pan.x = 0
joystick.camera.pan.y = 1
joystick.camera.rotate.x = 3
joystick.camera.rotate.y = 2
joystick.camera.zoom.in = 5
joystick.camera.zoom.out = 4