Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyTemplateManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyTemplateManager.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyTemplateManager.js (nonexistent)
@@ -1,52 +0,0 @@
-/**
- * System component which loads the technology and the aura data files
- */
-function TechnologyTemplateManager() {}
-
-TechnologyTemplateManager.prototype.Schema =
- "";
-
-TechnologyTemplateManager.prototype.Init = function()
-{
- this.allTechs = {};
- this.allAuras = {};
- var techNames = this.ListAllTechs();
- for (var i in techNames)
- this.GetTemplate(techNames[i]);
-};
-
-TechnologyTemplateManager.prototype.GetTemplate = function(template)
-{
- if (!this.allTechs[template])
- {
- this.allTechs[template] = Engine.ReadJSONFile("technologies/" + template + ".json");
- if (!this.allTechs[template])
- error("Failed to load technology \"" + template + "\"");
- }
-
- return this.allTechs[template];
-};
-
-TechnologyTemplateManager.prototype.GetAuraTemplate = function(template)
-{
- if (!this.allAuras[template])
- {
- this.allAuras[template] = Engine.ReadJSONFile("auras/" + template + ".json");
- if (!this.allAuras[template])
- error("Failed to load aura \"" + template + "\"");
- }
-
- return this.allAuras[template];
-};
-
-TechnologyTemplateManager.prototype.ListAllTechs = function()
-{
- return Engine.FindJSONFiles("technologies", true);
-};
-
-TechnologyTemplateManager.prototype.GetAllTechs = function()
-{
- return this.allTechs;
-};
-
-Engine.RegisterSystemComponentType(IID_TechnologyTemplateManager, "TechnologyTemplateManager", TechnologyTemplateManager);
Index: ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.cpp (revision 18099)
+++ ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.cpp (nonexistent)
@@ -1,39 +0,0 @@
-/* Copyright (C) 2012 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 "ICmpTechnologyTemplateManager.h"
-
-#include "simulation2/system/InterfaceScripted.h"
-#include "simulation2/scripting/ScriptComponent.h"
-
-BEGIN_INTERFACE_WRAPPER(TechnologyTemplateManager)
-END_INTERFACE_WRAPPER(TechnologyTemplateManager)
-
-class CCmpTechnologyTemplateManagerScripted : public ICmpTechnologyTemplateManager
-{
-public:
- DEFAULT_SCRIPT_WRAPPER(TechnologyTemplateManagerScripted)
-
- virtual void GetAllTechs(JS::MutableHandleValue ret)
- {
- return m_Script.CallRef("GetAllTechs", ret);
- }
-};
-
-REGISTER_COMPONENT_SCRIPT_WRAPPER(TechnologyTemplateManagerScripted)
Property changes on: ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.cpp
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.h (revision 18099)
+++ ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.h (nonexistent)
@@ -1,38 +0,0 @@
-/* Copyright (C) 2012 Wildfire Games.
- * This file is part of 0 A.D.
- *
- * 0 A.D. is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 2 of the License, or
- * (at your option) any later version.
- *
- * 0 A.D. is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with 0 A.D. If not, see .
- */
-
-#ifndef INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER
-#define INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER
-
-#include "simulation2/system/Interface.h"
-
-#include "maths/Fixed.h"
-
-/**
- * Technology template manager interface.
- * (This interface only includes the functions needed by native code for accessing
- * technology template data, the associated logic is handled in scripts)
- */
-class ICmpTechnologyTemplateManager : public IComponent
-{
-public:
- virtual void GetAllTechs(JS::MutableHandleValue ret) = 0;
-
- DECLARE_INTERFACE_TYPE(TechnologyTemplateManager)
-};
-
-#endif // INCLUDED_ICMPTECHNOLOGYTEMPLATEMANAGER
Property changes on: ps/trunk/source/simulation2/components/ICmpTechnologyTemplateManager.h
___________________________________________________________________
Deleted: svn:eol-style
## -1 +0,0 ##
-native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/simulation/components/Auras.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 18100)
@@ -1,414 +1,414 @@
function Auras() {}
Auras.prototype.Schema =
"" +
"tokens" +
"" +
"";
Auras.prototype.Init = function()
{
- let cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
+ let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
this.auras = {};
this.affectedPlayers = {};
let auraNames = this.GetAuraNames();
for (let name of auraNames)
{
this.affectedPlayers[name] = [];
- this.auras[name] = cmpTechnologyTemplateManager.GetAuraTemplate(name);
+ this.auras[name] = cmpDataTemplateManager.GetAuraTemplate(name);
}
// In case of autogarrisoning, this component can be called before ownership is set.
// So it needs to be completely initialised from the start.
this.Clean();
};
// We can modify identifier if we want stackable auras in some case.
Auras.prototype.GetModifierIdentifier = function(name)
{
if (this.auras[name].stackable)
return name + this.entity;
return name;
};
Auras.prototype.GetDescriptions = function()
{
let auraNames = this.GetAuraNames();
var ret = {};
for (let name of auraNames)
{
let aura = this.auras[name];
if (aura.auraName)
ret[aura.auraName] = aura.auraDescription || null;
}
return ret;
};
Auras.prototype.GetAuraNames = function()
{
return this.template._string.split(/\s+/);
};
Auras.prototype.GetOverlayIcon = function(name)
{
return this.auras[name].overlayIcon || "";
};
Auras.prototype.GetAffectedEntities = function(name)
{
return this[name].targetUnits;
};
Auras.prototype.GetRange = function(name)
{
if (!this.IsRangeAura(name))
return undefined;
if (this.IsGlobalAura(name))
return -1; // -1 is infinite range
return +this.auras[name].radius;
};
Auras.prototype.GetClasses = function(name)
{
return this.auras[name].affects;
};
Auras.prototype.GetModifications = function(name)
{
return this.auras[name].modifications;
};
Auras.prototype.GetAffectedPlayers = function(name)
{
return this.affectedPlayers[name];
};
Auras.prototype.CalculateAffectedPlayers = function(name)
{
var affectedPlayers = this.auras[name].affectedPlayers || ["Player"];
this.affectedPlayers[name] = [];
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
var numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (var i = 0; i < numPlayers; ++i)
{
for (let p of affectedPlayers)
{
if (p == "Player" ? cmpPlayer.GetPlayerID() == i : cmpPlayer["Is" + p](i))
{
this.affectedPlayers[name].push(i);
break;
}
}
}
};
Auras.prototype.CanApply = function(name)
{
if (!this.auras[name].requiredTechnology)
return true;
let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager || !cmpTechnologyManager.IsTechnologyResearched(this.auras[name].requiredTechnology))
return false;
return true;
};
Auras.prototype.HasFormationAura = function()
{
return this.GetAuraNames().some(n => this.IsFormationAura(n));
};
Auras.prototype.HasGarrisonAura = function()
{
return this.GetAuraNames().some(n => this.IsGarrisonAura(n));
};
Auras.prototype.HasGarrisonedUnitsAura = function()
{
return this.GetAuraNames().some(n => this.IsGarrisonedUnitsAura(n));
};
Auras.prototype.GetType = function(name)
{
return this.auras[name].type;
};
Auras.prototype.IsFormationAura = function(name)
{
return this.GetType(name) == "formation";
};
Auras.prototype.IsGarrisonAura = function(name)
{
return this.GetType(name) == "garrison";
};
Auras.prototype.IsGarrisonedUnitsAura = function(name)
{
return this.GetType(name) == "garrisonedUnits";
};
Auras.prototype.IsRangeAura = function(name)
{
// A global aura is also treated as a range aura with infinite range.
return ["range", "global"].indexOf(this.GetType(name)) != -1;
};
Auras.prototype.IsGlobalAura = function(name)
{
return this.GetType(name) == "global";
};
/**
* clean all bonuses. Remove the old ones and re-apply the new ones
*/
Auras.prototype.Clean = function()
{
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var auraNames = this.GetAuraNames();
let targetUnitsClone = {};
// remove all bonuses
for (let name of auraNames)
{
targetUnitsClone[name] = [];
if (!this[name])
continue;
if (this[name].targetUnits)
targetUnitsClone[name] = this[name].targetUnits.slice();
if (this.IsGlobalAura(name))
this.RemoveTemplateBonus(name);
this.RemoveBonus(name, this[name].targetUnits);
if (this[name].rangeQuery)
cmpRangeManager.DestroyActiveQuery(this[name].rangeQuery);
}
for (let name of auraNames)
{
// only calculate the affected players on re-applying the bonuses
// this makes sure the template bonuses are removed from the correct players
this.CalculateAffectedPlayers(name);
// initialise range query
this[name] = {};
this[name].targetUnits = [];
this[name].isApplied = this.CanApply(name);
var affectedPlayers = this.GetAffectedPlayers(name);
if (!affectedPlayers.length)
continue;
if (!this.IsRangeAura(name))
{
this.ApplyBonus(name, targetUnitsClone[name]);
continue;
}
this[name].rangeQuery = cmpRangeManager.CreateActiveQuery(
this.entity,
0,
this.GetRange(name),
affectedPlayers,
IID_Identity,
cmpRangeManager.GetEntityFlagMask("normal")
);
cmpRangeManager.EnableActiveQuery(this[name].rangeQuery);
if (this.IsGlobalAura(name))
{
this.ApplyTemplateBonus(name, affectedPlayers);
// Add self to your own query for consistency with templates.
this.OnRangeUpdate({
"tag": this[name].rangeQuery,
"added": [this.entity],
"removed": []
});
}
}
};
Auras.prototype.GiveMembersWithValidClass = function(auraName, entityList)
{
var match = this.GetClasses(auraName);
return entityList.filter(ent => {
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), match);
});
};
Auras.prototype.OnRangeUpdate = function(msg)
{
var auraNames = this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery);
for (let name of auraNames)
{
this.ApplyBonus(name, msg.added);
this.RemoveBonus(name, msg.removed);
}
};
Auras.prototype.OnGarrisonedUnitsChanged = function(msg)
{
var auraNames = this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n));
for (let name of auraNames)
{
this.ApplyBonus(name, msg.added);
this.RemoveBonus(name, msg.removed);
}
};
Auras.prototype.ApplyFormationBonus = function(memberList)
{
var auraNames = this.GetAuraNames().filter(n => this.IsFormationAura(n));
for (let name of auraNames)
this.ApplyBonus(name, memberList);
};
Auras.prototype.ApplyGarrisonBonus = function(structure)
{
var auraNames = this.GetAuraNames().filter(n => this.IsGarrisonAura(n));
for (let name of auraNames)
this.ApplyBonus(name, [structure]);
};
Auras.prototype.ApplyTemplateBonus = function(name, players)
{
if (!this[name].isApplied)
return;
if (!this.IsGlobalAura(name))
return;
var modifications = this.GetModifications(name);
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
var classes = this.GetClasses(name);
for (let mod of modifications)
for (let player of players)
cmpAuraManager.ApplyTemplateBonus(mod.value, player, classes, mod, this.GetModifierIdentifier(name));
};
Auras.prototype.RemoveFormationBonus = function(memberList)
{
var auraNames = this.GetAuraNames().filter(n => this.IsFormationAura(n));
for (let name of auraNames)
this.RemoveBonus(name, memberList);
};
Auras.prototype.RemoveGarrisonBonus = function(structure)
{
var auraNames = this.GetAuraNames().filter(n => this.IsGarrisonAura(n));
for (let name of auraNames)
this.RemoveBonus(name, [structure]);
};
Auras.prototype.RemoveTemplateBonus = function(name)
{
if (!this[name].isApplied)
return;
if (!this.IsGlobalAura(name))
return;
var modifications = this.GetModifications(name);
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
var classes = this.GetClasses(name);
var players = this.GetAffectedPlayers(name);
for (let mod of modifications)
for (let player of players)
cmpAuraManager.RemoveTemplateBonus(mod.value, player, classes, this.GetModifierIdentifier(name));
};
Auras.prototype.ApplyBonus = function(name, ents)
{
var validEnts = this.GiveMembersWithValidClass(name, ents);
if (!validEnts.length)
return;
this[name].targetUnits = this[name].targetUnits.concat(validEnts);
if (!this[name].isApplied)
return;
var modifications = this.GetModifications(name);
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
for (let mod of modifications)
cmpAuraManager.ApplyBonus(mod.value, validEnts, mod, this.GetModifierIdentifier(name));
// update status bars if this has an icon
if (!this.GetOverlayIcon(name))
return;
for (let ent of validEnts)
{
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.AddAuraSource(this.entity, name);
}
};
Auras.prototype.RemoveBonus = function(name, ents)
{
var validEnts = this.GiveMembersWithValidClass(name, ents);
if (!validEnts.length)
return;
this[name].targetUnits = this[name].targetUnits.filter(v => validEnts.indexOf(v) == -1);
if (!this[name].isApplied)
return;
var modifications = this.GetModifications(name);
var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager);
for (let mod of modifications)
cmpAuraManager.RemoveBonus(mod.value, validEnts, this.GetModifierIdentifier(name));
// update status bars if this has an icon
if (!this.GetOverlayIcon(name))
return;
for (let ent of validEnts)
{
var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RemoveAuraSource(this.entity, name);
}
};
Auras.prototype.OnOwnershipChanged = function(msg)
{
this.Clean();
};
Auras.prototype.OnDiplomacyChanged = function(msg)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == msg.player)
this.Clean();
};
Auras.prototype.OnGlobalResearchFinished = function(msg)
{
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || cmpOwnership.GetOwner() != msg.player)
return;
let auraNames = this.GetAuraNames();
let needsClean = false;
for (let name of auraNames)
{
let requiredTech = this.auras[name].requiredTechnology;
if (requiredTech && requiredTech == msg.tech)
{
needsClean = true;
break;
}
}
if (needsClean)
this.Clean();
};
Engine.RegisterComponentType(IID_Auras, "Auras", Auras);
Index: ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (revision 18100)
@@ -0,0 +1,59 @@
+/**
+ * System component which loads the technology and the aura data files
+ */
+function DataTemplateManager() {}
+
+DataTemplateManager.prototype.Schema =
+ "";
+
+DataTemplateManager.prototype.Init = function()
+{
+ this.allTechs = {};
+ this.allAuras = {};
+ var techNames = this.ListAllTechs();
+ for (var i in techNames)
+ this.GetTechnologyTemplate(techNames[i]);
+};
+
+DataTemplateManager.prototype.Serialize = null; // we have no dynamic state to save
+
+DataTemplateManager.prototype.Deserialize = function()
+{
+ this.Init();
+};
+
+DataTemplateManager.prototype.GetTechnologyTemplate = function(template)
+{
+ if (!this.allTechs[template])
+ {
+ this.allTechs[template] = Engine.ReadJSONFile("technologies/" + template + ".json");
+ if (!this.allTechs[template])
+ error("Failed to load technology \"" + template + "\"");
+ }
+
+ return this.allTechs[template];
+};
+
+DataTemplateManager.prototype.GetAuraTemplate = function(template)
+{
+ if (!this.allAuras[template])
+ {
+ this.allAuras[template] = Engine.ReadJSONFile("auras/" + template + ".json");
+ if (!this.allAuras[template])
+ error("Failed to load aura \"" + template + "\"");
+ }
+
+ return this.allAuras[template];
+};
+
+DataTemplateManager.prototype.ListAllTechs = function()
+{
+ return Engine.FindJSONFiles("technologies", true);
+};
+
+DataTemplateManager.prototype.GetAllTechs = function()
+{
+ return this.allTechs;
+};
+
+Engine.RegisterSystemComponentType(IID_DataTemplateManager, "DataTemplateManager", DataTemplateManager);
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 18100)
@@ -1,1927 +1,1927 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronised for the biggest part
// So most of the attributes shouldn't be serialized
// Return an object with a small selection of deterministic data
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function(player)
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerEntityLimits = Engine.QueryInterface(playerEnt, IID_EntityLimits);
let cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
let phase = "";
let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
// store player ally/neutral/enemy data as arrays
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"heroes": cmpPlayer.GetHeroes(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedResearch() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = 4 * cmpTerrain.GetTilesPerSide();
// Add timeElapsed
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
// Add ceasefire info
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
// Add the game type
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.gameType = cmpEndGameManager.GetGameType();
// Add bartering prices
let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterPrices = cmpBarter.GetPrices();
// Add basic statistics to each player
for (let i = 0; i < numPlayers; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function(player)
{
// Get basic simulation info
let ret = this.GetSimulationState();
// Add statistics to each player
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let n = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < n; ++i)
{
let playerEnt = cmpPlayerManager.GetPlayerByID(i);
let cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics();
}
return ret;
};
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
else
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function(player)
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"template": template,
"alertRaiser": null,
"builder": null,
"identity": null,
"fogging": null,
"foundation": null,
"garrisonHolder": null,
"gate": null,
"guard": null,
"market": null,
"mirage": null,
"pack": null,
"player": -1,
"position": null,
"production": null,
"rallyPoint": null,
"resourceCarrying": null,
"rotation": null,
"trader": null,
"unitAI": null,
"visibility": null,
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"visibleClasses": cmpIdentity.GetVisibleClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
{
ret.position = cmpPosition.GetPosition();
ret.rotation = cmpPosition.GetRotation();
}
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints());
ret.needsHeal = !cmpHealth.IsUnhealable();
ret.canDelete = !cmpHealth.IsUndeletable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = Engine.QueryInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval"),
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress(),
};
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"queue": cmpProductionQueue.GetQueue(),
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods(),
"requiredGoods": cmpTrader.GetRequiredGoods()
};
let cmpFogging = Engine.QueryInterface(ent, IID_Fogging);
if (cmpFogging)
{
if (cmpFogging.IsMiraged(player))
ret.fogging = { "mirage": cmpFogging.GetMirage(player) };
else
ret.fogging = { "mirage": null };
}
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"progress": cmpFoundation.GetBuildPercentage(),
"numBuilders": cmpFoundation.GetNumBuilders()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = { "numBuilders": cmpRepairable.GetNumBuilders() };
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
{
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"possibleStances": cmpUnitAI.GetPossibleStances(),
"isIdle":cmpUnitAI.IsIdle(),
};
// Add some information needed for ungarrisoning
if (cmpUnitAI.IsGarrisoned() && ret.player !== undefined)
ret.template = "p" + ret.player + "&" + ret.template;
}
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities(),
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked(),
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = {
"level": cmpAlertRaiser.GetLevel(),
"canIncreaseLevel": cmpAlertRaiser.CanIncreaseLevel(),
"hasRaisedAlert": cmpAlertRaiser.HasRaisedAlert(),
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
return ret;
};
/**
* Get additionnal entity info, rarely used in the gui
*/
GuiInterface.prototype.GetExtendedEntityState = function(player, ent)
{
let ret = {
"armour": null,
"attack": null,
"barterMarket": null,
"buildingAI": null,
"healer": null,
"obstruction": null,
"turretParent":null,
"promotion": null,
"repairRate": null,
"buildRate": null,
"resourceDropsite": null,
"resourceGatherRates": null,
"resourceSupply": null,
};
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = cmpAttack.GetAttackStrengths(type);
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// not a ranged attack, set some defaults
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpUnitAI && cmpPosition && cmpPosition.IsInWorld())
{
// For units, take the rage in front of it, no spread. So angle = 0
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 0);
}
else if(cmpPosition && cmpPosition.IsInWorld())
{
// For buildings, take the average elevation around it. So angle = 2*pi
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, 2*Math.PI);
}
else
{
// not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
}
let cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver);
if (cmpArmour)
ret.armour = cmpArmour.GetArmourStrengths();
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (cmpAuras)
ret.auras = cmpAuras.GetDescriptions();
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (cmpObstruction)
ret.obstruction = {
"controlGroup": cmpObstruction.GetControlGroup(),
"controlGroup2": cmpObstruction.GetControlGroup2(),
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpRepairable = Engine.QueryInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairRate = cmpRepairable.GetRepairRate();
let cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.buildRate = cmpFoundation.GetBuildRate();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket"))
{
let cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
ret.barterMarket = { "prices": cmpBarter.GetPrices() };
}
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.healer = {
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses(),
};
return ret;
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2*Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, extendedName)
{
let name = extendedName;
// Special case for garrisoned units which have a extended template
if (extendedName.indexOf("&") != -1)
name = extendedName.slice(extendedName.indexOf("&")+1);
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, player, aurasTemplate);
// Add aura name and description loaded from JSON file
let auraNames = template.Auras._string.split(/\s+/);
- let cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
+ let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
for (let name of auraNames)
{
- let auraTemplate = cmpTechnologyTemplateManager.GetAuraTemplate(name);
+ let auraTemplate = cmpDataTemplateManager.GetAuraTemplate(name);
if (!auraTemplate)
{
- // the following warning is perhaps useless since it's yet done in TechnologyTemplateManager
+ // the following warning is perhaps useless since it's yet done in DataTemplateManager
warn("Tried to get data for invalid aura: " + name);
continue;
}
aurasTemplate[name] = {};
aurasTemplate[name].auraName = auraTemplate.auraName || null;
aurasTemplate[name].auraDescription = auraTemplate.auraDescription || null;
}
return GetTemplateDataHelper(template, player, aurasTemplate);
};
GuiInterface.prototype.GetTechnologyData = function(player, name)
{
- let cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
- let template = cmpTechnologyTemplateManager.GetTemplate(name);
+ let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
+ let template = cmpDataTemplateManager.GetTechnologyTemplate(name);
if (!template)
{
warn("Tried to get data for invalid technology: " + name);
return null;
}
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
return GetTechnologyDataHelper(template, cmpPlayer.GetCiv());
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player || player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
// Returns technologies that are being actively researched, along with
// which entity is researching them and how far along the research is.
GuiInterface.prototype.GetStartedResearch = function(player, viewedPlayer)
{
let cmpTechnologyManager = QueryPlayerIDInterface(viewedPlayer, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech in cmpTechnologyManager.GetTechsStarted())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
else
ret[tech].progress = 0;
}
return ret;
};
// Returns the battle state of the player.
GuiInterface.prototype.GetBattleState = function(player, viewedPlayer)
{
let cmpBattleDetection = QueryPlayerIDInterface(viewedPlayer, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
// Returns a list of ongoing attacks against the player.
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
return QueryPlayerIDInterface(player, IID_AttackDetection).GetIncomingAttacks();
};
// Used to show a red square over GUI elements you can't yet afford.
GuiInterface.prototype.GetNeededResources = function(player, data)
{
return QueryPlayerIDInterface(data.player || player).GetNeededResources(data.cost);
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default
if (notification.players == undefined)
{
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
notification.players = [-1];
for (let i = 1; i < numPlayers; ++i)
notification.players.push(i);
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(playerID, viewedPlayer)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// filter on players and time, since the delete timer might be executed with a delay
return this.timeNotifications.filter(n => n.players.indexOf(viewedPlayer) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
return QueryPlayerIDInterface(wantedPlayer).GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
for each (let ent in data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
// GetLastFormationName is named in a strange way as it (also) is
// the value of the current formation (see Formation.js LoadFormation)
if (cmpUnitAI && cmpUnitAI.GetLastFormationTemplate() == data.formationTemplate)
return true;
}
return false;
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for each (let ent in data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for each (let ent in cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let playerColors = {}; // cache of owner -> color map
for each (let ent in cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color:
let owner = -1;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r":1, "g":1, "b":1 };
let cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(owner), IID_Player);
if (cmpPlayer)
color = cmpPlayer.GetColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
}
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return [...this.entsWithAuraAndStatusBars];
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them
for each (let ent in this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities
for each (let ent in cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location)
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
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
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
pos = cmpRallyPoint.GetPositions()[0]; // may return undefined if no rally point is set
if (pos)
{
// Only update the position if we changed it (cmd.queued is set)
if ("queued" in cmd)
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition({ 'x': pos.x, 'y': pos.z }); // AddPosition takes a CFixedVector2D which has X/Y components, not X/Z
else
cmpRallyPointRenderer.SetPosition({ 'x': pos.x, 'y': pos.z }); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z
// rebuild the renderer when not set (when reading saved game or in case of building update)
else if (!cmpRallyPointRenderer.IsSet())
for each (let posi in cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition({ 'x': posi.x, 'y': posi.z });
cmpRallyPointRenderer.SetDisplayed(true);
// remember which entities have their rally points displayed so we can hide them again
this.entsRallyPointsDisplayed.push(ent);
}
}
};
/**
* 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 result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": [],
};
// 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)
{
let ent = this.placementEntity[1];
// Move the preview into the right location
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether building placement is valid
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
// Set it to a red shade if this is an invalid location
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* 'populationBonus': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false, // did the start position snap to anything?
"snappedEnt": INVALID_ENTITY, // if we snapped, was it to an entity? if yes, holds that entity's ID
};
// --------------------------------------------------------------------------------
// do some entity cache management and check for snapping
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// we're clearing the preview, clear the entity cache and bail
for (let tpl in this.placementWallEntities)
{
for each (let ent in this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// keep template data around
}
return false;
}
else
{
// Move all existing cached entities outside of the world and reset their use count
for (let tpl in this.placementWallEntities)
{
for each (let ent in this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before
for each (let tpl in wallSet.templates)
{
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, tpl),
};
// ensure that the loaded template data contains a wallPiece component
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
}
// prevent division by zero errors further on if the start and end positions are the same
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5; // determined through trial and error
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// clear the single-building preview entity (we'll be rolling our own)
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// calculate wall placement and position preview entities
let result = {
"pieces": [],
"cost": { "food": 0, "wood": 0, "stone": 0, "metal": 0, "population": 0, "populationBonus": 0, "time": 0 },
};
let previewEntities = [];
if (end.pos)
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end); // see helpers/Walls.js
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length > 0 && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// if we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(startEntObstruction ? startEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true, // preview only, must not appear in the result
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": (previewEntities.length > 0 ? previewEntities[0].angle : this.placementWallLastAngle)
});
}
if (end.pos)
{
// Analogous to the starting side case above
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length-1].controlGroups = (previewEntities[previewEntities.length-1].controlGroups || []);
previewEntities[previewEntities.length-1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// if we're snapping to a foundation, add an extra preview tower and also set it to the same control group
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [(endEntObstruction ? endEntObstruction.GetControlGroup() : undefined)],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": (previewEntities.length > 0 ? previewEntities[previewEntities.length-1].angle : this.placementWallLastAngle)
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
let numRequiredPieces = 0; // number of entities that are required to build the entire wall, regardless of validity
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
// allocate new entity
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
// reuse an existing one
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// move piece to right location
// TODO: consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// if this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i-1].pos.x, previewEntities[i-1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i+1].pos.x, previewEntities[i+1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
// check whether this wall piece can be validly positioned here
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region
// TODO: should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta
let visible = (cmpRangeManager.GetLosVisibility(ent, player) != "hidden");
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement
validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success);
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: we should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
result.cost.food += tplData.cost.food;
result.cost.wood += tplData.cost.wood;
result.cost.stone += tplData.cost.stone;
result.cost.metal += tplData.cost.metal;
result.cost.population += tplData.cost.population;
result.cost.populationBonus += tplData.cost.populationBonus;
result.cost.time += tplData.cost.time;
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// see if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest
// (TODO: break unlikely ties by choosing the lowest entity ID)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for each (let ent in data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (template.BuildRestrictions.Category == "Dock")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let playerEntities = cmpRangeManager.GetEntitiesByPlayer(data.viewedPlayer).filter(entity => {
let cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return false;
let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity || !cmpIdentity.HasClass(data.idleClass))
return false;
return true;
});
let idleUnits = [];
for (let ent of playerEntities)
{
if (ent <= data.prevUnit|0 || data.excludeUnits.indexOf(ent) > -1)
continue;
idleUnits.push(ent);
if (data.limit && idleUnits.length >= data.limit)
break;
}
return idleUnits;
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
{
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
}
else if (!firstMarket)
{
result = { "type": "set first" };
}
else if (!secondMarket)
{
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
}
else
{
// Else both markets are not null and target is different from them
result = { "type": "set first" };
}
return result;
};
GuiInterface.prototype.CanCapture = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
let owner = QueryOwnerInterface(data.entity).GetPlayerID();
let cmpCapturable = QueryMiragedInterface(data.target, IID_Capturable);
if (cmpCapturable && cmpCapturable.CanCapture(owner) && cmpAttack.GetAttackTypes().indexOf("Capture") != -1)
return cmpAttack.CanAttack(data.target);
return false;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
if (!cmpAttack)
return false;
let cmpEntityPlayer = QueryOwnerInterface(data.entity, IID_Player);
let cmpTargetPlayer = QueryOwnerInterface(data.target, IID_Player);
if (!cmpEntityPlayer || !cmpTargetPlayer)
return false;
// if the owner is an enemy, it's up to the attack component to decide
if (cmpEntityPlayer.IsEnemy(cmpTargetPlayer.GetPlayerID()))
return cmpAttack.CanAttack(data.target);
return false;
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for each (let ent in data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player, viewedPlayer)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(viewedPlayer).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for each (let ent in traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player, viewedPlayer)
{
return QueryPlayerIDInterface(viewedPlayer).GetTradingGoods();
};
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.)
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetExtendedEntityState": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"GetTechnologyData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanCapture": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 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.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 18100)
@@ -1,825 +1,825 @@
var g_ProgressInterval = 1000;
const MAX_QUEUE_SIZE = 16;
function ProductionQueue() {}
ProductionQueue.prototype.Schema =
"Allows the building to train new units and research technologies" +
"" +
"0.7" +
"" +
"\n units/{civ}_support_female_citizen\n units/{civ}_support_trader\n units/athen_infantry_spearman_b\n " +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"";
ProductionQueue.prototype.Init = function()
{
this.nextID = 1;
this.queue = [];
// Queue items are:
// {
// "id": 1,
// "player": 1, // who paid for this batch; we need this to cope with refunds cleanly
// "unitTemplate": "units/example",
// "count": 10,
// "neededSlots": 3, // number of population slots missing for production to begin
// "resources": { "wood": 100, ... }, // resources per unit, multiply by count to get total
// "population": 1, // population per unit, multiply by count to get total
// "productionStarted": false, // true iff we have reserved population
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
//
// {
// "id": 1,
// "player": 1, // who paid for this research; we need this to cope with refunds cleanly
// "technologyTemplate": "example_tech",
// "resources": { "wood": 100, ... }, // resources needed for research
// "productionStarted": false, // true iff production has started
// "timeTotal": 15000, // msecs
// "timeRemaining": 10000, // msecs
// }
this.timer = undefined; // g_ProgressInterval msec timer, active while the queue is non-empty
this.paused = false;
this.entityCache = [];
this.spawnNotified = false;
this.alertRaiser = undefined;
};
ProductionQueue.prototype.PutUnderAlert = function(raiser)
{
this.alertRaiser = raiser;
};
ProductionQueue.prototype.ResetAlert = function()
{
this.alertRaiser = undefined;
};
/*
* Returns list of entities that can be trained by this building.
*/
ProductionQueue.prototype.GetEntitiesList = function()
{
return this.entitiesList;
};
ProductionQueue.prototype.CalculateEntitiesList = function()
{
this.entitiesList = [];
if (!this.template.Entities)
return;
var string = this.template.Entities._string;
if (!string)
return;
// Replace the "{civ}" codes with this entity's civ ID
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpPlayer = QueryOwnerInterface(this.entity);
if (!cmpPlayer)
return;
var entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/);
// filter out disabled and invalid entities
var disabledEntities = cmpPlayer.GetDisabledTemplates();
entitiesList = entitiesList.filter(
function(v) { return !disabledEntities[v] && cmpTemplateManager.TemplateExists(v); }
);
// check if some templates need to show their advanced or elite version
var upgradeTemplate = function(templateName)
{
var template = cmpTemplateManager.GetTemplate(templateName);
while (template && template.Promotion !== undefined)
{
var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template);
if (requiredXp > 0)
break;
templateName = template.Promotion.Entity;
template = cmpTemplateManager.GetTemplate(templateName);
}
return templateName;
};
for (let templateName of entitiesList)
this.entitiesList.push(upgradeTemplate(templateName));
for (let item of this.queue)
if (item.unitTemplate)
item.unitTemplate = upgradeTemplate(item.unitTemplate);
};
/*
* Returns list of technologies that can be researched by this building.
*/
ProductionQueue.prototype.GetTechnologiesList = function()
{
if (!this.template.Technologies)
return [];
var string = this.template.Technologies._string;
if (!string)
return [];
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
if (!cmpTechnologyManager)
return [];
var cmpPlayer = QueryOwnerInterface(this.entity);
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
if (!cmpPlayer || !cmpIdentity)
return [];
var techs = string.split(/\s+/);
// Remove any technologies that can't be searched by this civ (after capture for example)
techs = techs.filter(tech => {
let reqs = cmpTechnologyManager.GetTechnologyTemplate(tech).requirements || null;
return cmpTechnologyManager.CheckTechnologyRequirements(reqs, true);
});
var techList = [];
var superseded = {}; // Stores the tech which supersedes the key
var disabledTechnologies = cmpPlayer.GetDisabledTechnologies();
// Add any top level technologies to an array which corresponds to the displayed icons
// Also store what a technology is superceded by in the superceded object {"tech1":"techWhichSupercedesTech1", ...}
for (var i in techs)
{
var tech = techs[i];
if (disabledTechnologies && disabledTechnologies[tech])
continue;
var template = cmpTechnologyManager.GetTechnologyTemplate(tech);
if (!template.supersedes || techs.indexOf(template.supersedes) === -1)
techList.push(tech);
else
superseded[template.supersedes] = tech;
}
// Now make researched/in progress techs invisible
for (var i in techList)
{
var tech = techList[i];
while (this.IsTechnologyResearchedOrInProgress(tech))
{
tech = superseded[tech];
}
techList[i] = tech;
}
var ret = [];
// This inserts the techs into the correct positions to line up the technology pairs
for (var i = 0; i < techList.length; i++)
{
var tech = techList[i];
if (!tech)
{
ret[i] = undefined;
continue;
}
var template = cmpTechnologyManager.GetTechnologyTemplate(tech);
if (template.top)
ret[i] = {"pair": true, "top": template.top, "bottom": template.bottom};
else
ret[i] = tech;
}
return ret;
};
ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech)
{
if (!tech)
return false;
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
var template = cmpTechnologyManager.GetTechnologyTemplate(tech);
if (template.top)
{
return (cmpTechnologyManager.IsTechnologyResearched(template.top) || cmpTechnologyManager.IsInProgress(template.top)
|| cmpTechnologyManager.IsTechnologyResearched(template.bottom) || cmpTechnologyManager.IsInProgress(template.bottom));
}
else
{
return (cmpTechnologyManager.IsTechnologyResearched(tech) || cmpTechnologyManager.IsInProgress(tech));
}
};
/*
* Adds a new batch of identical units to train or a technology to research to the production queue.
*/
ProductionQueue.prototype.AddBatch = function(templateName, type, count, metadata)
{
// TODO: there should probably be a limit on the number of queued batches
// TODO: there should be a way for the GUI to determine whether it's going
// to be possible to add a batch (based on resource costs and length limits)
var cmpPlayer = QueryOwnerInterface(this.entity);
if (this.queue.length < MAX_QUEUE_SIZE)
{
if (type == "unit")
{
// Find the template data so we can determine the build costs
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return;
if (template.Promotion)
{
var requiredXp = ApplyValueModificationsToTemplate("Promotion/RequiredXp", +template.Promotion.RequiredXp, cmpPlayer.GetPlayerID(), template);
if (requiredXp == 0)
{
this.AddBatch(template.Promotion.Entity, type, count, metadata);
return;
}
}
// Apply a time discount to larger batches.
var timeMult = this.GetBatchTime(count);
// We need the costs after tech modifications
// Obviously we don't have the entities yet, so we must use template data
var costs = {};
var totalCosts = {};
var buildTime = ApplyValueModificationsToTemplate("Cost/BuildTime", +template.Cost.BuildTime, cmpPlayer.GetPlayerID(), template);
var time = timeMult * buildTime;
for (var r in template.Cost.Resources)
{
costs[r] = ApplyValueModificationsToTemplate("Cost/Resources/"+r, +template.Cost.Resources[r], cmpPlayer.GetPlayerID(), template);
totalCosts[r] = Math.floor(count * costs[r]);
}
var population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, cmpPlayer.GetPlayerID(), template);
// TrySubtractResources should report error to player (they ran out of resources)
if (!cmpPlayer.TrySubtractResources(totalCosts))
return;
// Update entity count in the EntityLimits component
if (template.TrainingRestrictions)
{
var unitCategory = template.TrainingRestrictions.Category;
var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.ChangeCount(unitCategory, count);
}
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"unitTemplate": templateName,
"count": count,
"metadata": metadata,
"resources": costs,
"population": population,
"productionStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("TrainingQueued", {"playerid": cmpPlayer.GetPlayerID(), "unitTemplate": templateName, "count": count, "metadata": metadata, "trainerEntity": this.entity});
}
else if (type == "technology")
{
// Load the technology template
- var cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
- var template = cmpTechnologyTemplateManager.GetTemplate(templateName);
+ var cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
+ var template = cmpDataTemplateManager.GetTechnologyTemplate(templateName);
if (!template)
return;
var cmpPlayer = QueryOwnerInterface(this.entity);
var time = template.researchTime * cmpPlayer.GetCheatTimeMultiplier();
var cost = {};
for each (var r in ["food", "wood", "stone", "metal"])
cost[r] = Math.floor(template.cost[r]);
// TrySubtractResources should report error to player (they ran out of resources)
if (!cmpPlayer.TrySubtractResources(cost))
return;
// Tell the technology manager that we have started researching this so that people can't research the same
// thing twice.
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.QueuedResearch(templateName, this.entity);
if (this.queue.length == 0)
cmpTechnologyManager.StartedResearch(templateName);
this.queue.push({
"id": this.nextID++,
"player": cmpPlayer.GetPlayerID(),
"count": 1,
"technologyTemplate": templateName,
"resources": deepcopy(template.cost), // need to copy to avoid serialization problems
"productionStarted": false,
"timeTotal": time*1000,
"timeRemaining": time*1000,
});
// Call the related trigger event
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.CallEvent("ResearchQueued", {"playerid": cmpPlayer.GetPlayerID(), "technologyTemplate": templateName, "researcherEntity": this.entity});
}
else
{
warn("Tried to add invalid item of type \"" + type + "\" and template \"" + templateName + "\" to a production queue");
return;
}
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
// If this is the first item in the queue, start the timer
if (!this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {});
}
}
else
{
var notification = {"players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("The production queue is full."), "translateMessage": true };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
}
};
/*
* Removes an existing batch of units from the production queue.
* Refunds resource costs and population reservations.
*/
ProductionQueue.prototype.RemoveBatch = function(id)
{
// Destroy any cached entities (those which didn't spawn for some reason)
for (var i = 0; i < this.entityCache.length; ++i)
{
Engine.DestroyEntity(this.entityCache[i]);
}
this.entityCache = [];
for (var i = 0; i < this.queue.length; ++i)
{
var item = this.queue[i];
if (item.id != id)
continue;
// Now we've found the item to remove
var cmpPlayer = QueryPlayerIDInterface(item.player);
// Update entity count in the EntityLimits component
if (item.unitTemplate)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(item.unitTemplate);
if (template.TrainingRestrictions)
{
var unitCategory = template.TrainingRestrictions.Category;
var cmpPlayerEntityLimits = QueryPlayerIDInterface(item.player, IID_EntityLimits);
cmpPlayerEntityLimits.ChangeCount(unitCategory, -item.count);
}
}
// Refund the resource cost for this batch
var totalCosts = {};
var cmpStatisticsTracker = QueryPlayerIDInterface(item.player, IID_StatisticsTracker);
for each (var r in ["food", "wood", "stone", "metal"])
{
totalCosts[r] = Math.floor(item.count * item.resources[r]);
if (cmpStatisticsTracker)
cmpStatisticsTracker.IncreaseResourceUsedCounter(r, -totalCosts[r]);
}
cmpPlayer.AddResources(totalCosts);
// Remove reserved population slots if necessary
if (item.productionStarted && item.unitTemplate)
cmpPlayer.UnReservePopulationSlots(item.population * item.count);
// Mark the research as stopped if we cancel it
if (item.technologyTemplate)
{
// item.player is used as this.entity's owner may be invalid (deletion, etc.)
var cmpTechnologyManager = QueryPlayerIDInterface(item.player, IID_TechnologyManager);
cmpTechnologyManager.StoppedResearch(item.technologyTemplate);
}
// Remove from the queue
// (We don't need to remove the timer - it'll expire if it discovers the queue is empty)
this.queue.splice(i, 1);
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
return;
}
};
/*
* Returns basic data from all batches in the production queue.
*/
ProductionQueue.prototype.GetQueue = function()
{
var out = [];
for each (var item in this.queue)
{
out.push({
"id": item.id,
"unitTemplate": item.unitTemplate,
"technologyTemplate": item.technologyTemplate,
"count": item.count,
"neededSlots": item.neededSlots,
"progress": 1 - ( item.timeRemaining / (item.timeTotal || 1) ),
"timeRemaining": item.timeRemaining,
"metadata": item.metadata,
});
}
return out;
};
/*
* Removes all existing batches from the queue.
*/
ProductionQueue.prototype.ResetQueue = function()
{
// Empty the production queue and refund all the resource costs
// to the player. (This is to avoid players having to micromanage their
// buildings' queues when they're about to be destroyed or captured.)
while (this.queue.length)
this.RemoveBatch(this.queue[0].id);
};
/*
* Returns batch build time.
*/
ProductionQueue.prototype.GetBatchTime = function(batchSize)
{
var cmpPlayer = QueryOwnerInterface(this.entity);
var batchTimeModifier = ApplyValueModificationsToEntity("ProductionQueue/BatchTimeModifier", +this.template.BatchTimeModifier, this.entity);
// TODO: work out what equation we should use here.
return Math.pow(batchSize, batchTimeModifier) * cmpPlayer.GetCheatTimeMultiplier();
};
ProductionQueue.prototype.OnOwnershipChanged = function(msg)
{
if (msg.from != -1)
{
// Unset flag that previous owner's training may be blocked
var cmpPlayer = QueryPlayerIDInterface(msg.from);
if (cmpPlayer && this.queue.length > 0)
cmpPlayer.UnBlockTraining();
}
if (msg.to != -1)
this.CalculateEntitiesList();
// Reset the production queue whenever the owner changes.
// (This should prevent players getting surprised when they capture
// an enemy building, and then loads of the enemy's civ's soldiers get
// created from it. Also it means we don't have to worry about
// updating the reserved pop slots.)
this.ResetQueue();
};
ProductionQueue.prototype.OnCivChanged = function()
{
this.CalculateEntitiesList();
};
ProductionQueue.prototype.OnDestroy = function()
{
// Reset the queue to refund any resources
this.ResetQueue();
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/*
* This function creates the entities and places them in world if possible
* and returns the number of successfully created entities.
* (some of these entities may be garrisoned directly if autogarrison, the others are spawned).
*/
ProductionQueue.prototype.SpawnUnits = function(templateName, count, metadata)
{
var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
var createdEnts = [];
var spawnedEnts = [];
if (this.entityCache.length == 0)
{
// We need entities to test spawning, but we don't want to waste resources,
// so only create them once and use as needed
for (var i = 0; i < count; ++i)
{
var ent = Engine.AddEntity(templateName);
this.entityCache.push(ent);
// Decrement entity count in the EntityLimits component
// since it will be increased by EntityLimits.OnGlobalOwnershipChanged function,
// i.e. we replace a 'trained' entity to an 'alive' one
var cmpTrainingRestrictions = Engine.QueryInterface(ent, IID_TrainingRestrictions);
if (cmpTrainingRestrictions)
{
var unitCategory = cmpTrainingRestrictions.GetCategory();
var cmpPlayerEntityLimits = QueryOwnerInterface(this.entity, IID_EntityLimits);
cmpPlayerEntityLimits.ChangeCount(unitCategory,-1);
}
}
}
var cmpAutoGarrison = undefined;
if (cmpRallyPoint)
{
var data = cmpRallyPoint.GetData()[0];
if (data && data.target && data.target == this.entity && data.command == "garrison")
cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
}
for (var i = 0; i < count; ++i)
{
var ent = this.entityCache[0];
if (cmpAutoGarrison && cmpAutoGarrison.PerformGarrison(ent))
{
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
cmpUnitAI.Autogarrison(this.entity);
}
else
{
var pos = cmpFootprint.PickSpawnPoint(ent);
if (pos.y < 0)
{
// Fail: there wasn't any space to spawn the unit
break;
}
else
{
// Successfully spawned
let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpNewOwnership.SetOwner(cmpOwnership.GetOwner());
let cmpNewPosition = Engine.QueryInterface(ent, IID_Position);
cmpNewPosition.JumpTo(pos.x, pos.z);
// TODO: what direction should they face in?
spawnedEnts.push(ent);
}
}
var cmpPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
cmpPlayerStatisticsTracker.IncreaseTrainedUnitsCounter(ent);
// Play a sound, but only for the first in the batch (to avoid nasty phasing effects)
if (createdEnts.length == 0)
PlaySound("trained", ent);
this.entityCache.shift();
createdEnts.push(ent);
}
if (spawnedEnts.length > 0 && !cmpAutoGarrison)
{
// If a rally point is set, walk towards it (in formation) using a suitable command based on where the
// rally point is placed.
if (cmpRallyPoint)
{
var rallyPos = cmpRallyPoint.GetPositions()[0];
if (rallyPos)
{
var commands = GetRallyPointCommands(cmpRallyPoint, spawnedEnts);
for each(var com in commands)
ProcessCommand(cmpOwnership.GetOwner(), com);
}
}
}
if (createdEnts.length > 0)
{
Engine.PostMessage(this.entity, MT_TrainingFinished, {
"entities": createdEnts,
"owner": cmpOwnership.GetOwner(),
"metadata": metadata,
});
if(this.alertRaiser && spawnedEnts.length > 0)
{
var cmpAlertRaiser = Engine.QueryInterface(this.alertRaiser, IID_AlertRaiser);
if(cmpAlertRaiser)
cmpAlertRaiser.UpdateUnits(spawnedEnts);
}
}
return createdEnts.length;
};
/*
* Increments progress on the first batch in the production queue, and blocks the
* queue if population limit is reached or some units failed to spawn.
*/
ProductionQueue.prototype.ProgressTimeout = function(data)
{
// Check if the production is paused (eg the entity is garrisoned)
if (this.paused)
return;
// Allocate the 1000msecs to as many queue items as it takes
// until we've used up all the time (so that we work accurately
// with items that take fractions of a second)
var time = g_ProgressInterval;
var cmpPlayer = QueryOwnerInterface(this.entity);
while (time > 0 && this.queue.length)
{
var item = this.queue[0];
if (!item.productionStarted)
{
// If the item is a unit then do population checks
if (item.unitTemplate)
{
// If something change population cost
var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(item.unitTemplate);
item.population = ApplyValueModificationsToTemplate("Cost/Population", +template.Cost.Population, item.player, template);
// Batch's training hasn't started yet.
// Try to reserve the necessary population slots
item.neededSlots = cmpPlayer.TryReservePopulationSlots(item.population * item.count);
if (item.neededSlots)
{
// Not enough slots available - don't train this batch now
// (we'll try again on the next timeout)
// Set flag that training is blocked
cmpPlayer.BlockTraining();
break;
}
// Unset flag that training is blocked
cmpPlayer.UnBlockTraining();
}
if (item.technologyTemplate)
{
// Mark the research as started.
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.StartedResearch(item.technologyTemplate);
}
item.productionStarted = true;
if (item.unitTemplate)
Engine.PostMessage(this.entity, MT_TrainingStarted, {"entity": this.entity});
}
// If we won't finish the batch now, just update its timer
if (item.timeRemaining > time)
{
item.timeRemaining -= time;
// send a message for the AIs.
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
break;
}
if (item.unitTemplate)
{
var numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata);
if (numSpawned == item.count)
{
// All entities spawned, this batch finished
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
time -= item.timeRemaining;
this.queue.shift();
// Unset flag that training is blocked
cmpPlayer.UnBlockTraining();
this.spawnNotified = false;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
else
{
if (numSpawned > 0)
{
// Only partially finished
cmpPlayer.UnReservePopulationSlots(item.population * numSpawned);
item.count -= numSpawned;
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
// Some entities failed to spawn
// Set flag that training is blocked
cmpPlayer.BlockTraining();
if (!this.spawnNotified)
{
var cmpPlayer = QueryOwnerInterface(this.entity);
var notification = { "players": [cmpPlayer.GetPlayerID()], "message": markForTranslation("Can't find free space to spawn trained units"), "translateMessage": true };
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
cmpGUIInterface.PushNotification(notification);
this.spawnNotified = true;
}
break;
}
}
else if (item.technologyTemplate)
{
var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager);
cmpTechnologyManager.ResearchTechnology(item.technologyTemplate);
var template = cmpTechnologyManager.GetTechnologyTemplate(item.technologyTemplate);
if (template && template.soundComplete)
{
var cmpSoundManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager);
if (cmpSoundManager)
cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity);
}
time -= item.timeRemaining;
this.queue.shift();
Engine.PostMessage(this.entity, MT_ProductionQueueChanged, { });
}
}
// If the queue's empty, delete the timer, else repeat it
if (this.queue.length == 0)
{
this.timer = undefined;
// Unset flag that training is blocked
// (This might happen when the player unqueues all batches)
cmpPlayer.UnBlockTraining();
}
else
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, data);
}
};
ProductionQueue.prototype.PauseProduction = function()
{
this.timer = undefined;
this.paused = true;
};
ProductionQueue.prototype.UnpauseProduction = function()
{
this.paused = false;
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", g_ProgressInterval, {});
};
ProductionQueue.prototype.OnValueModification = function(msg)
{
// if the promotion requirements of units is changed,
// update the entities list so that automatically promoted units are shown
// appropriately in the list
if (msg.component == "Promotion")
this.CalculateEntitiesList();
};
ProductionQueue.prototype.OnDisabledTemplatesChanged = function(msg)
{
// if the disabled templates of the player is changed,
// update the entities list so that this is reflected there
this.CalculateEntitiesList();
};
Engine.RegisterComponentType(IID_ProductionQueue, "ProductionQueue", ProductionQueue);
Index: ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 18100)
@@ -1,468 +1,468 @@
function TechnologyManager() {}
TechnologyManager.prototype.Schema =
"";
TechnologyManager.prototype.Serialize = function()
{
// The modifications cache will be affected by property reads from the GUI and other places so we shouldn't
// serialize it.
var ret = {};
for (var i in this)
{
if (this.hasOwnProperty(i))
ret[i] = this[i];
}
ret.modificationCache = {};
return ret;
};
TechnologyManager.prototype.Init = function()
{
this.researchedTechs = {}; // technologies which have been researched
this.researchQueued = {}; // technologies which are queued for research
this.researchStarted = {}; // technologies which are being researched currently (non-queued)
// This stores the modifications to unit stats from researched technologies
// Example data: {"ResourceGatherer/Rates/food.grain": [
// {"multiply": 1.15, "affects": ["Female", "Infantry Swordsman"]},
// {"add": 2}
// ]}
this.modifications = {};
this.modificationCache = {}; // Caches the values after technologies have been applied
// e.g. { "Attack/Melee/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...}
// where 5 and 7 are entity id's
this.classCounts = {}; // stores the number of entities of each Class
this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e.
// {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...}
// Some technologies are automatically researched when their conditions are met. They have no cost and are
// researched instantly. This allows civ bonuses and more complicated technologies.
this.autoResearchTech = {};
- var allTechs = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager).GetAllTechs();
+ var allTechs = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager).GetAllTechs();
for (var key in allTechs)
{
if (allTechs[key].autoResearch || allTechs[key].top)
this.autoResearchTech[key] = allTechs[key];
}
};
TechnologyManager.prototype.OnUpdate = function()
{
this.UpdateAutoResearch();
};
// This function checks if the requirements of any autoresearch techs are met and if they are it researches them
TechnologyManager.prototype.UpdateAutoResearch = function()
{
- var cmpTechTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
+ var cmpDataTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
for (var key in this.autoResearchTech)
{
- var tech = cmpTechTempMan.GetTemplate(key);
+ var tech = cmpDataTempMan.GetTechnologyTemplate(key);
if ((tech.autoResearch && this.CanResearch(key))
|| (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom))))
{
delete this.autoResearchTech[key];
this.ResearchTechnology(key);
return; // We will have recursively handled any knock-on effects so can just return
}
}
};
TechnologyManager.prototype.GetTechnologyTemplate = function(tech)
{
- return Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager).GetTemplate(tech);
+ return Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager).GetTechnologyTemplate(tech);
};
// Checks an entity template to see if its technology requirements have been met
TechnologyManager.prototype.CanProduce = function (templateName)
{
var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempManager.GetTemplate(templateName);
if (template.Identity && template.Identity.RequiredTechnology)
return this.IsTechnologyResearched(template.Identity.RequiredTechnology);
// If there is no required technology then this entity can be produced
return true;
};
TechnologyManager.prototype.IsTechnologyResearched = function(tech)
{
return (this.researchedTechs[tech] !== undefined);
};
// Checks the requirements for a technology to see if it can be researched at the current time
TechnologyManager.prototype.CanResearch = function(tech)
{
var template = this.GetTechnologyTemplate(tech);
if (!template)
{
warn("Technology \"" + tech + "\" does not exist");
return false;
}
// The technology which this technology supersedes is required
if (template.supersedes && !this.IsTechnologyResearched(template.supersedes))
return false;
if (template.top && this.IsInProgress(template.top) ||
template.bottom && this.IsInProgress(template.bottom))
return false;
if (template.pair && !this.CanResearch(template.pair))
return false;
if (this.IsInProgress(tech))
return false;
if (this.IsTechnologyResearched(tech))
return false;
return this.CheckTechnologyRequirements(template.requirements || null);
};
/**
* Private function for checking a set of requirements is met
* @param reqs Object of technology requirements as given by the technology template
* @param civonly A boolean set to true if only the civ requirement is checked
*
* @return true if the requirements are checked
* false otherwise
*/
TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly)
{
// If there are no requirements then all requirements are met
if (!reqs)
return true;
if (reqs.all)
return reqs.all.every(r => this.CheckTechnologyRequirements(r, civonly));
if (reqs.any)
return reqs.any.some(r => this.CheckTechnologyRequirements(r, civonly));
if (reqs.civ)
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
return cmpPlayer && cmpPlayer.GetCiv() == reqs.civ;
}
if (reqs.notciv)
{
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
return cmpPlayer && cmpPlayer.GetCiv() != reqs.notciv;
}
if (civonly)
return true;
if (reqs.tech)
return this.IsTechnologyResearched(reqs.tech);
if (reqs.class && reqs.numberOfTypes)
return this.typeCountsByClass[reqs.class] &&
Object.keys(this.typeCountsByClass[reqs.class]).length >= reqs.numberOfTypes;
if (reqs.class && reqs.number)
return this.classCounts[reqs.class] &&
this.classCounts[reqs.class] >= reqs.number;
// The technologies requirements are not a recognised format
error("Bad requirements " + uneval(reqs));
return false;
};
TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
{
// This automatically updates classCounts and typeCountsByClass
var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID();
if (msg.to == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (!cmpIdentity)
return;
var classes = cmpIdentity.GetClassesList();
// don't use foundations for the class counts but check if techs apply (e.g. health increase)
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
for (let cls of classes)
{
this.classCounts[cls] = this.classCounts[cls] || 0;
this.classCounts[cls] += 1;
this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {};
this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0;
this.typeCountsByClass[cls][template] += 1;
}
}
// Newly created entity, check if any researched techs might apply
// (only do this for new entities because even if an entity is converted or captured,
// we want it to maintain whatever technologies previously applied)
if (msg.from == -1)
{
var modifiedComponents = {};
for (var name in this.modifications)
{
// We only need to find one one tech per component for a match
var modifications = this.modifications[name];
var component = name.split("/")[0];
for (let modif of modifications)
if (DoesModificationApply(modif, classes))
{
if (!modifiedComponents[component])
modifiedComponents[component] = [];
modifiedComponents[component].push(name);
}
}
// Send mesage(s) to the entity so it knows about researched techs
for (var component in modifiedComponents)
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
}
}
if (msg.from == playerID)
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);
// don't use foundations for the class counts
if (!Engine.QueryInterface(msg.entity, IID_Foundation))
{
var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
if (cmpIdentity)
{
var classes = cmpIdentity.GetClassesList();
for (let cls of classes)
{
this.classCounts[cls] -= 1;
if (this.classCounts[cls] <= 0)
delete this.classCounts[cls];
this.typeCountsByClass[cls][template] -= 1;
if (this.typeCountsByClass[cls][template] <= 0)
delete this.typeCountsByClass[cls][template];
}
}
}
this.clearModificationCache(msg.entity);
}
};
// Marks a technology as researched. Note that this does not verify that the requirements are met.
TechnologyManager.prototype.ResearchTechnology = function(tech)
{
this.StoppedResearch(tech); // The tech is no longer being currently researched
var template = this.GetTechnologyTemplate(tech);
if (!template)
{
error("Tried to research invalid technology: " + uneval(tech));
return;
}
var modifiedComponents = {};
this.researchedTechs[tech] = template;
// store the modifications in an easy to access structure
if (template.modifications)
{
var affects = [];
if (template.affects && template.affects.length > 0)
{
for (let affect of template.affects)
{
// Put the list of classes into an array for convenient access
affects.push(affect.split(/\s+/));
}
}
else
{
affects.push([]);
}
// We add an item to this.modifications for every modification in the template.modifications array
for (var i in template.modifications)
{
var modification = template.modifications[i];
if (!this.modifications[modification.value])
this.modifications[modification.value] = [];
var modAffects = affects.slice();
if (modification.affects)
{
var extraAffects = modification.affects.split(/\s+/);
for (var a in modAffects)
modAffects[a] = modAffects[a].concat(extraAffects);
}
var mod = {"affects": modAffects};
// copy the modification data into our new data structure
for (var j in modification)
if (j !== "value" && j !== "affects")
mod[j] = modification[j];
this.modifications[modification.value].push(mod);
var component = modification.value.split("/")[0];
if (!modifiedComponents[component])
modifiedComponents[component] = [];
modifiedComponents[component].push(modification.value);
this.modificationCache[modification.value] = {};
}
}
if (template.replaces && template.replaces.length > 0)
{
for (var i of template.replaces)
{
if (!i || this.IsTechnologyResearched(i))
continue;
var template = this.GetTechnologyTemplate(i);
this.researchedTechs[i] = template;
// Change the EntityLimit if any
let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined)
{
let playerID = cmpPlayer.GetPlayerID();
let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(i);
}
}
}
this.UpdateAutoResearch();
var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined)
return;
var playerID = cmpPlayer.GetPlayerID();
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
var ents = cmpRangeManager.GetEntitiesByPlayer(playerID);
// Change the EntityLimit if any
var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
if (cmpPlayerEntityLimits)
cmpPlayerEntityLimits.UpdateLimitsFromTech(tech);
// always send research finished message
Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech});
for (var component in modifiedComponents)
{
Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]});
Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]});
}
};
// Clears the cached data for an entity from the modifications cache
TechnologyManager.prototype.clearModificationCache = function(ent)
{
for (var valueName in this.modificationCache)
delete this.modificationCache[valueName][ent];
};
// Caching layer in front of ApplyModificationsWorker
// Note: be careful with the type of curValue, if it should be a numerical
// value and is derived from template data, you must convert the string
// from the template to a number using the + operator, before calling
// this function!
TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent)
{
if (!this.modificationCache[valueName])
this.modificationCache[valueName] = {};
if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue)
{
this.modificationCache[valueName][ent] = {"origValue": curValue};
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
// Ensure that preview or construction entites have the same properties as the final building
if (templateName.indexOf("preview|") > -1 || templateName.indexOf("construction|") > -1 )
templateName = templateName.slice(templateName.indexOf("|") + 1);
this.modificationCache[valueName][ent].newValue = GetTechModifiedProperty(this.modifications, cmpTemplateManager.GetTemplate(templateName), valueName, curValue);
}
return this.modificationCache[valueName][ent].newValue;
};
// Alternative version of ApplyModifications, applies to templates instead of entities
TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template)
{
return GetTechModifiedProperty(this.modifications, template, valueName, curValue);
};
// Marks a technology as being queued for research
TechnologyManager.prototype.QueuedResearch = function(tech, researcher)
{
this.researchQueued[tech] = researcher;
};
// Marks a technology as actively being researched
TechnologyManager.prototype.StartedResearch = function(tech)
{
this.researchStarted[tech] = true;
};
// Marks a technology as not being currently researched
TechnologyManager.prototype.StoppedResearch = function(tech)
{
delete this.researchQueued[tech];
delete this.researchStarted[tech];
};
// Checks whether a technology is set to be researched
TechnologyManager.prototype.IsInProgress = function(tech)
{
if (this.researchQueued[tech])
return true;
else
return false;
};
// Get all techs that are currently being researched
TechnologyManager.prototype.GetTechsStarted = function()
{
return this.researchStarted;
};
// Gets the entity currently researching a technology
TechnologyManager.prototype.GetResearcher = function(tech)
{
if (this.researchQueued[tech])
return this.researchQueued[tech];
return undefined;
};
// Get helper data for tech modifications
TechnologyManager.prototype.GetTechModifications = function()
{
return this.modifications;
};
// called by GUIInterface for PlayerData. AI use.
TechnologyManager.prototype.GetQueuedResearch = function()
{
return this.researchQueued;
};
TechnologyManager.prototype.GetStartedResearch = function()
{
return this.researchStarted;
};
TechnologyManager.prototype.GetResearchedTechs = function()
{
return this.researchedTechs;
};
TechnologyManager.prototype.GetClassCounts = function()
{
return this.classCounts;
};
TechnologyManager.prototype.GetTypeCountsByClass = function()
{
return this.typeCountsByClass;
};
Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);
Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 18100)
@@ -1,75 +1,75 @@
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("ValueModification.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/AuraManager.js");
Engine.LoadComponentScript("interfaces/TechnologyManager.js");
Engine.LoadComponentScript("Auras.js");
Engine.LoadComponentScript("AuraManager.js");
var playerEnt1 = 5;
var auraEnt = 20;
var targetEnt = 30;
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
GetPlayerByID: function() { return playerEnt1; },
GetNumPlayers: function() { return 2; },
});
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) {
return 1;
},
EnableActiveQuery: function(id) { },
ResetActiveQuery: function(id) { if (mode == 0) return []; else return [enemy]; },
DisableActiveQuery: function(id) { },
GetEntityFlagMask: function(identifier) { },
});
-AddMock(SYSTEM_ENTITY, IID_TechnologyTemplateManager, {
+AddMock(SYSTEM_ENTITY, IID_DataTemplateManager, {
GetAuraTemplate: function(name) {
if (name == "test1")
return {
"type": "global",
"affects": ["CorrectClass"],
"modifications": [ { "value": "Component/Value", "add": 1 } ],
"auraName": "name",
"auraDescription": "description"
};
return {};
},
});
AddMock(playerEnt1, IID_Player, {
IsAlly: function(id) { return id == 1; },
IsEnemy: function(id) { return id != 1; },
GetPlayerID: function() { return 1; },
});
AddMock(targetEnt, IID_Identity, {
GetClassesList: function() { return ["CorrectClass"]; },
});
AddMock(auraEnt, IID_Position, {
GetPosition2D: function() { return new Vector2D(); },
});
AddMock(targetEnt, IID_Position, {
// target ent at 20m distance from aura ent
GetPosition2D: function() { return new Vector2D(); },
});
AddMock(targetEnt, IID_Ownership, {
GetOwner: function() { return 1; },
});
AddMock(auraEnt, IID_Ownership, {
GetOwner: function() { return 1; },
});
ConstructComponent(SYSTEM_ENTITY, "AuraManager", {});
var auras = ConstructComponent(auraEnt, "Auras", {_string: "test1"});
// send the rangeManager message
auras.OnRangeUpdate({"tag": 1, "added": [30], "removed": []});
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 1, targetEnt), 2);
Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 18099)
+++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 18100)
@@ -1,166 +1,166 @@
function Cheat(input)
{
var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
if (!cmpPlayerManager || input.player < 0)
return;
var playerEnt = cmpPlayerManager.GetPlayerByID(input.player);
if (playerEnt == INVALID_ENTITY)
return;
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
if (!cmpPlayer)
return;
var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
if (!cmpPlayer.GetCheatsEnabled())
return;
switch(input.action)
{
case "addresource":
cmpPlayer.AddResource(input.text, input.parameter);
return;
case "revealmap":
var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
cmpRangeManager.SetLosRevealAll(-1, true);
return;
case "maxpopulation":
cmpPlayer.SetPopulationBonuses(500);
return;
case "changemaxpopulation":
cmpPlayer.SetMaxPopulation(500);
return;
case "convertunit":
for (let ent of input.selected)
{
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
cmpOwnership.SetOwner(cmpPlayer.GetPlayerID());
}
return;
case "killunits":
for (let ent of input.selected)
{
let cmpHealth = Engine.QueryInterface(ent, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
else
Engine.DestroyEntity(ent);
}
return;
case "defeatplayer":
var playerEnt = cmpPlayerManager.GetPlayerByID(input.parameter);
if (playerEnt == INVALID_ENTITY)
return;
Engine.PostMessage(playerEnt, MT_PlayerDefeated, { "playerId": input.parameter });
return;
case "createunits":
var cmpProductionQueue = input.selected.length && Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
if (!cmpProductionQueue)
{
cmpGuiInterface.PushNotification({
"type": "text",
"players": [input.player],
"message": markForTranslation("You need to select a building that trains units."),
"translateMessage": true
});
return;
}
for (let i = 0; i < Math.min(input.parameter, cmpPlayer.GetMaxPopulation() - cmpPlayer.GetPopulationCount()); ++i)
cmpProductionQueue.SpawnUnits(input.templates[i % input.templates.length], 1, null);
return;
case "fastactions":
cmpPlayer.SetCheatTimeMultiplier((cmpPlayer.GetCheatTimeMultiplier() == 1) ? 0.01 : 1);
return;
case "changespeed":
cmpPlayer.SetCheatTimeMultiplier(input.parameter);
return;
case "changephase":
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (!cmpTechnologyManager)
return;
// store the phase we want in the next input parameter
var parameter;
if (!cmpTechnologyManager.IsTechnologyResearched("phase_town"))
parameter = "phase_town";
else if (!cmpTechnologyManager.IsTechnologyResearched("phase_city"))
parameter = "phase_city";
else
return;
// check if specialised tech exists (like phase_town_athen)
- var cmpTechnologyTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TechnologyTemplateManager);
- if (cmpTechnologyTemplateManager.ListAllTechs().indexOf(parameter + "_" + cmpPlayer.civ) > -1)
+ var cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
+ if (cmpDataTemplateManager.ListAllTechs().indexOf(parameter + "_" + cmpPlayer.civ) > -1)
parameter += "_" + cmpPlayer.civ;
Cheat({ "player": input.player, "action": "researchTechnology", "parameter": parameter, "selected": input.selected });
return;
case "researchTechnology":
if (!input.parameter.length)
return;
var techname = input.parameter;
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
if (!cmpTechnologyManager)
return;
// check, if building is selected
if (input.selected[0])
{
var cmpProductionQueue = Engine.QueryInterface(input.selected[0], IID_ProductionQueue);
if (cmpProductionQueue)
{
// try to spilt the input
var tmp = input.parameter.split(/\s+/);
var number = +tmp[0];
var pair = tmp.length > 1 && (tmp[1] == "top" || tmp[1] == "bottom") ? tmp[1] : "top"; // use top as default value
// check, if valid number was parsed.
if (number || number === 0)
{
// get name of tech
var techs = cmpProductionQueue.GetTechnologiesList();
if (number > 0 && number <= techs.length)
{
var tech = techs[number-1];
if (!tech)
return;
// get name of tech
if (tech.pair)
techname = tech[pair];
else
techname = tech;
}
else
return;
}
}
}
// check, if technology exists
var template = cmpTechnologyManager.GetTechnologyTemplate(techname);
if (!template)
return;
// check, if technology is already researched
if (!cmpTechnologyManager.IsTechnologyResearched(techname))
cmpTechnologyManager.ResearchTechnology(techname);
return;
case "metaCheat":
for (let resource of ["food", "wood", "metal", "stone"])
Cheat({ "player": input.player, "action": "addresource", "text": resource, "parameter": input.parameter });
Cheat({ "player": input.player, "action": "maxpopulation" });
Cheat({ "player": input.player, "action": "changemaxpopulation" });
Cheat({ "player": input.player, "action": "fastactions" });
for (let i=0; i<2; ++i)
Cheat({ "player": input.player, "action": "changephase", "selected": input.selected });
return;
default:
warn("Cheat '" + input.action + "' is not implemented");
return;
}
}
Engine.RegisterGlobal("Cheat", Cheat);
Index: ps/trunk/source/simulation2/TypeList.h
===================================================================
--- ps/trunk/source/simulation2/TypeList.h (revision 18099)
+++ ps/trunk/source/simulation2/TypeList.h (revision 18100)
@@ -1,194 +1,194 @@
/* Copyright (C) 2014 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
// MESSAGE: message types
// INTERFACE: component interface types
// COMPONENT: component types
// Components intended only for use in test cases:
// (The tests rely on the enum IDs, so don't change the order of these)
INTERFACE(Test1)
COMPONENT(Test1A)
COMPONENT(Test1B)
COMPONENT(Test1Scripted)
INTERFACE(Test2)
COMPONENT(Test2A)
COMPONENT(Test2Scripted)
// Message types:
MESSAGE(TurnStart)
MESSAGE(Update)
MESSAGE(Update_MotionFormation)
MESSAGE(Update_MotionUnit)
MESSAGE(Update_Final)
MESSAGE(Interpolate) // non-deterministic (use with caution)
MESSAGE(RenderSubmit) // non-deterministic (use with caution)
MESSAGE(ProgressiveLoad) // non-deterministic (use with caution)
MESSAGE(Deserialized) // non-deterministic (use with caution)
MESSAGE(Create)
MESSAGE(Destroy)
MESSAGE(OwnershipChanged)
MESSAGE(PositionChanged)
MESSAGE(InterpolatedPositionChanged)
MESSAGE(TerritoryPositionChanged)
MESSAGE(MotionChanged)
MESSAGE(RangeUpdate)
MESSAGE(TerrainChanged)
MESSAGE(VisibilityChanged)
MESSAGE(WaterChanged)
MESSAGE(ObstructionMapShapeChanged)
MESSAGE(TerritoriesChanged)
MESSAGE(PathResult)
MESSAGE(ValueModification)
MESSAGE(TemplateModification)
MESSAGE(VisionRangeChanged)
MESSAGE(MinimapPing)
MESSAGE(CinemaPathEnded)
MESSAGE(CinemaQueueEnded)
// TemplateManager must come before all other (non-test) components,
// so that it is the first to be (de)serialized
INTERFACE(TemplateManager)
COMPONENT(TemplateManager)
// Special component for script component types with no native interface
INTERFACE(UnknownScript)
COMPONENT(UnknownScript)
// In alphabetical order:
INTERFACE(AIInterface)
COMPONENT(AIInterfaceScripted)
INTERFACE(AIManager)
COMPONENT(AIManager)
INTERFACE(CinemaManager)
COMPONENT(CinemaManager)
INTERFACE(CommandQueue)
COMPONENT(CommandQueue)
INTERFACE(Decay)
COMPONENT(Decay)
INTERFACE(Fogging)
COMPONENT(FoggingScripted)
// Note: The VisualActor component relies on this component being initialized before itself, in order to support using
// an entity's footprint shape for the selection boxes. This dependency is not strictly necessary, but it does avoid
// some extra plumbing code to set up on-demand initialization. If you find yourself forced to break this dependency,
// see VisualActor's Init method for a description of how you can avoid it.
INTERFACE(Footprint)
COMPONENT(Footprint)
INTERFACE(GuiInterface)
COMPONENT(GuiInterfaceScripted)
INTERFACE(Identity)
COMPONENT(IdentityScripted)
INTERFACE(Minimap)
COMPONENT(Minimap)
INTERFACE(Mirage)
COMPONENT(MirageScripted)
INTERFACE(Motion)
COMPONENT(MotionBall)
COMPONENT(MotionScripted)
INTERFACE(Obstruction)
COMPONENT(Obstruction)
INTERFACE(ObstructionManager)
COMPONENT(ObstructionManager)
INTERFACE(OverlayRenderer)
COMPONENT(OverlayRenderer)
INTERFACE(Ownership)
COMPONENT(Ownership)
INTERFACE(ParticleManager)
COMPONENT(ParticleManager)
INTERFACE(Pathfinder)
COMPONENT(Pathfinder)
INTERFACE(Player)
COMPONENT(PlayerScripted)
INTERFACE(PlayerManager)
COMPONENT(PlayerManagerScripted)
INTERFACE(Position)
COMPONENT(Position) // must be before VisualActor
INTERFACE(ProjectileManager)
COMPONENT(ProjectileManager)
INTERFACE(RallyPointRenderer)
COMPONENT(RallyPointRenderer)
INTERFACE(RangeManager)
COMPONENT(RangeManager)
INTERFACE(Selectable)
COMPONENT(Selectable)
INTERFACE(Settlement)
COMPONENT(SettlementScripted)
INTERFACE(SoundManager)
COMPONENT(SoundManager)
INTERFACE(ValueModificationManager)
COMPONENT(ValueModificationManagerScripted)
-INTERFACE(TechnologyTemplateManager)
-COMPONENT(TechnologyTemplateManagerScripted)
+INTERFACE(DataTemplateManager)
+COMPONENT(DataTemplateManagerScripted)
INTERFACE(Terrain)
COMPONENT(Terrain)
INTERFACE(TerritoryInfluence)
COMPONENT(TerritoryInfluence)
INTERFACE(TerritoryManager)
COMPONENT(TerritoryManager)
INTERFACE(UnitMotion)
COMPONENT(UnitMotion) // must be after Obstruction
COMPONENT(UnitMotionScripted)
INTERFACE(UnitRenderer)
COMPONENT(UnitRenderer)
INTERFACE(Visibility)
COMPONENT(VisibilityScripted)
INTERFACE(Vision)
COMPONENT(Vision)
// Note: this component relies on the Footprint component being initialized before itself. See the comments above for
// the Footprint component to find out why.
INTERFACE(Visual)
COMPONENT(VisualActor) // must be after Ownership (dependency in Deserialize) and Vision (dependency in Init)
INTERFACE(WaterManager)
COMPONENT(WaterManager)
Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 18099)
+++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 18100)
@@ -1,1184 +1,1184 @@
/* Copyright (C) 2016 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see .
*/
#include "precompiled.h"
#include "simulation2/system/Component.h"
#include "ICmpAIManager.h"
#include "simulation2/MessageTypes.h"
#include "graphics/Terrain.h"
#include "lib/timer.h"
#include "lib/tex/tex.h"
#include "lib/allocators/shared_ptr.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/Util.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpCommandQueue.h"
#include "simulation2/components/ICmpObstructionManager.h"
#include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpTemplateManager.h"
-#include "simulation2/components/ICmpTechnologyTemplateManager.h"
+#include "simulation2/components/ICmpDataTemplateManager.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/LongPathfinder.h"
#include "simulation2/serialization/DebugSerializer.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "simulation2/serialization/SerializeTemplates.h"
/**
* @file
* Player AI interface.
* AI is primarily scripted, and the CCmpAIManager component defined here
* takes care of managing all the scripts.
*
* To avoid slow AI scripts causing jerky rendering, they are run in a background
* thread (maintained by CAIWorker) so that it's okay if they take a whole simulation
* turn before returning their results (though preferably they shouldn't use nearly
* that much CPU).
*
* CCmpAIManager grabs the world state after each turn (making use of AIInterface.js
* and AIProxy.js to decide what data to include) then passes it to CAIWorker.
* The AI scripts will then run asynchronously and return a list of commands to execute.
* Any attempts to read the command list (including indirectly via serialization)
* will block until it's actually completed, so the rest of the engine should avoid
* reading it for as long as possible.
*
* JS values are passed between the game and AI threads using ScriptInterface::StructuredClone.
*
* TODO: actually the thread isn't implemented yet, because performance hasn't been
* sufficiently problematic to justify the complexity yet, but the CAIWorker interface
* is designed to hopefully support threading when we want it.
*/
/**
* Implements worker thread for CCmpAIManager.
*/
class CAIWorker
{
private:
class CAIPlayer
{
NONCOPYABLE(CAIPlayer);
public:
CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty,
shared_ptr scriptInterface) :
m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty),
m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetJSRuntime())
{
}
bool Initialise()
{
// LoadScripts will only load each script once even though we call it for each player
if (!m_Worker.LoadScripts(m_AIName))
return false;
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
OsPath path = L"simulation/ai/" + m_AIName + L"/data.json";
JS::RootedValue metadata(cx);
m_Worker.LoadMetadata(path, &metadata);
if (metadata.isUndefined())
{
LOGERROR("Failed to create AI player: can't find %s", path.string8());
return false;
}
// Get the constructor name from the metadata
std::string moduleName;
std::string constructor;
JS::RootedValue objectWithConstructor(cx); // object that should contain the constructor function
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->HasProperty(metadata, "moduleName"))
{
LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8());
return false;
}
m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName);
if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor)
|| objectWithConstructor.isUndefined())
{
LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName);
return false;
}
if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor))
{
LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8());
return false;
}
// Get the constructor function from the loaded scripts
if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor)
|| ctor.isNull())
{
LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor);
return false;
}
m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent);
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
m_ScriptInterface->SetProperty(settings, "player", m_Player, false);
m_ScriptInterface->SetProperty(settings, "difficulty", m_Difficulty, false);
ENSURE(m_Worker.m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false);
JS::AutoValueVector argv(cx);
argv.append(settings.get());
m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj);
if (m_Obj.get().isNull())
{
LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor);
return false;
}
return true;
}
void Run(JS::HandleValue state, int playerID)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID);
}
// overloaded with a sharedAI part.
// javascript can handle both natively on the same function.
void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI);
}
void InitAI(JS::HandleValue state, JS::HandleValue SharedAI)
{
m_Commands.clear();
m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI);
}
CAIWorker& m_Worker;
std::wstring m_AIName;
player_id_t m_Player;
u8 m_Difficulty;
bool m_UseSharedComponent;
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptInterface;
JS::PersistentRootedValue m_Obj;
std::vector > m_Commands;
};
public:
struct SCommandSets
{
player_id_t player;
std::vector > commands;
};
CAIWorker() :
m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)),
m_TurnNum(0),
m_CommandsComputed(true),
m_HasLoadedEntityTemplates(false),
m_HasSharedComponent(false),
m_SerializablePrototypes(new ObjectIdCache(g_ScriptRuntime)),
m_EntityTemplates(g_ScriptRuntime->m_rt),
m_TechTemplates(g_ScriptRuntime->m_rt),
m_SharedAIObj(g_ScriptRuntime->m_rt),
m_PassabilityMapVal(g_ScriptRuntime->m_rt),
m_TerritoryMapVal(g_ScriptRuntime->m_rt)
{
m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface->LoadGlobalScripts();
m_ScriptInterface->SetCallbackData(static_cast (this));
m_SerializablePrototypes->init();
JS_AddExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
m_ScriptInterface->RegisterFunction("PostCommand");
m_ScriptInterface->RegisterFunction("IncludeModule");
m_ScriptInterface->RegisterFunction("ForceGC");
m_ScriptInterface->RegisterFunction("ComputePath");
m_ScriptInterface->RegisterFunction("GetConnectivityGrid");
m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage");
}
~CAIWorker()
{
JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this);
}
bool LoadScripts(const std::wstring& moduleName)
{
// Ignore modules that are already loaded
if (m_LoadedModules.find(moduleName) != m_LoadedModules.end())
return true;
// Mark this as loaded, to prevent it recursively loading itself
m_LoadedModules.insert(moduleName);
// Load and execute *.js
VfsPaths pathnames;
if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0)
{
LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName));
return false;
}
for (const VfsPath& path : pathnames)
{
if (!m_ScriptInterface->LoadGlobalScriptFile(path))
{
LOGERROR("Failed to load script %s", path.string8());
return false;
}
}
return true;
}
static void IncludeModule(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->LoadScripts(name);
}
static void PostCommand(ScriptInterface::CxPrivate* pCxPrivate, int playerid, JS::HandleValue cmd)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
self->PostCommand(playerid, cmd);
}
void PostCommand(int playerid, JS::HandleValue cmd)
{
for (size_t i=0; im_Player == playerid)
{
m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd));
return;
}
}
LOGERROR("Invalid playerid in PostCommand!");
}
static JS::Value ComputePath(ScriptInterface::CxPrivate* pCxPrivate,
JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
CFixedVector2D pos, goalPos;
std::vector waypoints;
JS::RootedValue retVal(cx);
self->m_ScriptInterface->FromJSVal(cx, position, pos);
self->m_ScriptInterface->FromJSVal(cx, goal, goalPos);
self->ComputePath(pos, goalPos, passClass, waypoints);
self->m_ScriptInterface->ToJSVal >(cx, &retVal, waypoints);
return retVal;
}
void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints)
{
WaypointPath ret;
PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y };
m_LongPathfinder.ComputePath(pos.X, pos.Y, pathGoal, passClass, ret);
for (Waypoint& wp : ret.m_Waypoints)
waypoints.emplace_back(wp.x, wp.z);
}
static JS::Value GetConnectivityGrid(ScriptInterface::CxPrivate* pCxPrivate, pass_class_t passClass)
{
ENSURE(pCxPrivate->pCBData);
CAIWorker* self = static_cast (pCxPrivate->pCBData);
JSContext* cx(self->m_ScriptInterface->GetContext());
JS::RootedValue retVal(cx);
self->m_ScriptInterface->ToJSVal >(cx, &retVal, self->m_LongPathfinder.GetConnectivityGrid(passClass));
return retVal;
}
static void ForceGC(ScriptInterface::CxPrivate* pCxPrivate)
{
PROFILE3("AI compute GC");
JS_GC(pCxPrivate->pScriptInterface->GetJSRuntime());
}
/**
* Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights).
*/
static void DumpImage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max)
{
// TODO: this is totally not threadsafe.
VfsPath filename = L"screenshots/aidump/" + name;
if (data.size() != w*h)
{
debug_warn(L"DumpImage: data size doesn't match w*h");
return;
}
if (max == 0)
{
debug_warn(L"DumpImage: max must not be 0");
return;
}
const size_t bpp = 8;
int flags = TEX_BOTTOM_UP|TEX_GREY;
const size_t img_size = w * h * bpp/8;
const size_t hdr_size = tex_hdr_size(filename);
shared_ptr buf;
AllocateAligned(buf, hdr_size+img_size, maxSectorSize);
Tex t;
if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0)
return;
u8* img = buf.get() + hdr_size;
for (size_t i = 0; i < data.size(); ++i)
img[i] = (u8)((data[i] * 255) / max);
tex_write(&t, filename);
}
void SetRNGSeed(u32 seed)
{
m_RNG.seed(seed);
}
bool TryLoadSharedComponent(bool hasTechs)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
// we don't need to load it.
if (!m_HasSharedComponent)
return false;
// reset the value so it can be used to determine if we actually initialized it.
m_HasSharedComponent = false;
if (LoadScripts(L"common-api"))
m_HasSharedComponent = true;
else
return false;
// mainly here for the error messages
OsPath path = L"simulation/ai/common-api/";
// Constructor name is SharedScript, it's in the module API3
// TODO: Hardcoding this is bad, we need a smarter way.
JS::RootedValue AIModule(cx);
JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject());
JS::RootedValue ctor(cx);
if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3");
return false;
}
if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor)
|| ctor.isUndefined())
{
LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript");
return false;
}
// Set up the data to pass as the constructor argument
JS::RootedValue settings(cx);
m_ScriptInterface->Eval(L"({})", &settings);
JS::RootedValue playersID(cx);
m_ScriptInterface->Eval(L"({})", &playersID);
for (size_t i = 0; i < m_Players.size(); ++i)
{
JS::RootedValue val(cx);
m_ScriptInterface->ToJSVal(cx, &val, m_Players[i]->m_Player);
m_ScriptInterface->SetPropertyInt(playersID, i, val, true);
}
m_ScriptInterface->SetProperty(settings, "players", playersID);
ENSURE(m_HasLoadedEntityTemplates);
m_ScriptInterface->SetProperty(settings, "templates", m_EntityTemplates, false);
if (hasTechs)
{
m_ScriptInterface->SetProperty(settings, "techTemplates", m_TechTemplates, false);
}
else
{
// won't get the tech templates directly.
JS::RootedValue fakeTech(cx);
m_ScriptInterface->Eval("({})", &fakeTech);
m_ScriptInterface->SetProperty(settings, "techTemplates", fakeTech, false);
}
JS::AutoValueVector argv(cx);
argv.append(settings);
m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj);
if (m_SharedAIObj.get().isNull())
{
LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript");
return false;
}
return true;
}
bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty)
{
shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface));
if (!ai->Initialise())
return false;
// this will be set to true if we need to load the shared Component.
if (!m_HasSharedComponent)
m_HasSharedComponent = ai->m_UseSharedComponent;
m_Players.push_back(ai);
return true;
}
bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
// this will be run last by InitGame.Js, passing the full game representation.
// For now it will run for the shared Component.
// This is NOT run during deserialization.
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
m_ScriptInterface->ReadStructuredClone(gameState, &state);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, passabilityMap);
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, territoryMap);
m_PassabilityMap = passabilityMap;
m_NonPathfindingPassClasses = nonPathfindingPassClassMasks;
m_PathfindingPassClasses = pathfindingPassClassMasks;
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
if (m_HasSharedComponent)
{
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state);
for (size_t i = 0; i < m_Players.size(); ++i)
{
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->InitAI(state, m_SharedAIObj);
}
}
return true;
}
void StartComputation(const shared_ptr& gameState,
const Grid& passabilityMap, const GridUpdateInformation& dirtinessInformations,
const Grid& territoryMap, bool territoryMapDirty,
const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks)
{
ENSURE(m_CommandsComputed);
m_GameState = gameState;
JSContext* cx = m_ScriptInterface->GetContext();
if (dirtinessInformations.dirty)
{
m_PassabilityMap = passabilityMap;
if (dirtinessInformations.globallyDirty)
m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
else
m_LongPathfinder.Update(&m_PassabilityMap, dirtinessInformations.dirtinessGrid);
ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap);
}
if (territoryMapDirty)
{
m_TerritoryMap = territoryMap;
ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, m_TerritoryMap);
}
m_CommandsComputed = false;
}
void WaitToFinishComputation()
{
if (!m_CommandsComputed)
{
PerformComputation();
m_CommandsComputed = true;
}
}
void GetCommands(std::vector& commands)
{
WaitToFinishComputation();
commands.clear();
commands.resize(m_Players.size());
for (size_t i = 0; i < m_Players.size(); ++i)
{
commands[i].player = m_Players[i]->m_Player;
commands[i].commands = m_Players[i]->m_Commands;
}
}
void RegisterTechTemplates(const shared_ptr& techTemplates)
{
m_ScriptInterface->ReadStructuredClone(techTemplates, &m_TechTemplates);
}
void LoadEntityTemplates(const std::vector >& templates)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
m_HasLoadedEntityTemplates = true;
m_ScriptInterface->Eval("({})", &m_EntityTemplates);
JS::RootedValue val(cx);
for (size_t i = 0; i < templates.size(); ++i)
{
templates[i].second->ToJSVal(cx, false, &val);
m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true);
}
// Since the template data is shared between AI players, freeze it
// to stop any of them changing it and confusing the other players
m_ScriptInterface->FreezeObject(m_EntityTemplates, true);
}
void Serialize(std::ostream& stream, bool isDebug)
{
WaitToFinishComputation();
if (isDebug)
{
CDebugSerializer serializer(*m_ScriptInterface, stream);
serializer.Indent(4);
SerializeState(serializer);
}
else
{
CStdSerializer serializer(*m_ScriptInterface, stream);
// TODO: see comment in Deserialize()
serializer.SetSerializablePrototypes(m_SerializablePrototypes);
SerializeState(serializer);
}
}
void SerializeState(ISerializer& serializer)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
std::stringstream rngStream;
rngStream << m_RNG;
serializer.StringASCII("rng", rngStream.str(), 0, 32);
serializer.NumberU32_Unbounded("turn", m_TurnNum);
serializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
JS::RootedValue sharedData(cx);
if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData))
LOGERROR("AI shared script Serialize call failed");
serializer.ScriptVal("sharedData", &sharedData);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player);
serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty);
serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size());
for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j)
{
JS::RootedValue val(cx);
m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val);
serializer.ScriptVal("command", &val);
}
bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize");
if (hasCustomSerialize)
{
JS::RootedValue scriptData(cx);
if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData))
LOGERROR("AI script Serialize call failed");
serializer.ScriptVal("data", &scriptData);
}
else
{
serializer.ScriptVal("data", &m_Players[i]->m_Obj);
}
}
// AI pathfinder
SerializeMap()(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(serializer, "pathfinding pass classes", m_PathfindingPassClasses);
serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W);
serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H);
serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data,
m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData));
}
void Deserialize(std::istream& stream, u32 numAis)
{
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad
CStdDeserializer deserializer(*m_ScriptInterface, stream);
m_PlayerMetadata.clear();
m_Players.clear();
std::string rngString;
std::stringstream rngStream;
deserializer.StringASCII("rng", rngString, 0, 32);
rngStream << rngString;
rngStream >> m_RNG;
deserializer.NumberU32_Unbounded("turn", m_TurnNum);
deserializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
TryLoadSharedComponent(false);
JS::RootedValue sharedData(cx);
deserializer.ScriptVal("sharedData", &sharedData);
if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData))
LOGERROR("AI shared script Deserialize call failed");
}
for (size_t i = 0; i < numAis; ++i)
{
std::wstring name;
player_id_t player;
u8 difficulty;
deserializer.String("name", name, 1, 256);
deserializer.NumberI32_Unbounded("player", player);
deserializer.NumberU8_Unbounded("difficulty",difficulty);
if (!AddPlayer(name, player, difficulty))
throw PSERROR_Deserialize_ScriptError();
u32 numCommands;
deserializer.NumberU32_Unbounded("num commands", numCommands);
m_Players.back()->m_Commands.reserve(numCommands);
for (size_t j = 0; j < numCommands; ++j)
{
JS::RootedValue val(cx);
deserializer.ScriptVal("command", &val);
m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val));
}
// TODO: this is yucky but necessary while the AIs are sharing data between contexts;
// ideally a new (de)serializer instance would be created for each player
// so they would have a single, consistent script context to use and serializable
// prototypes could be stored in their ScriptInterface
deserializer.SetSerializablePrototypes(m_DeserializablePrototypes);
bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize");
if (hasCustomDeserialize)
{
JS::RootedValue scriptData(cx);
deserializer.ScriptVal("data", &scriptData);
if (m_Players[i]->m_UseSharedComponent)
{
if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj))
LOGERROR("AI script Deserialize call failed");
}
else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData))
{
LOGERROR("AI script deserialize() call failed");
}
}
else
{
deserializer.ScriptVal("data", &m_Players.back()->m_Obj);
}
}
// AI pathfinder
SerializeMap()(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
SerializeMap()(deserializer, "pathfinding pass classes", m_PathfindingPassClasses);
u16 mapW, mapH;
deserializer.NumberU16_Unbounded("pathfinder grid w", mapW);
deserializer.NumberU16_Unbounded("pathfinder grid h", mapH);
m_PassabilityMap = Grid(mapW, mapH);
deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData));
m_LongPathfinder.Reload(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses);
}
int getPlayerSize()
{
return m_Players.size();
}
void RegisterSerializablePrototype(std::wstring name, JS::HandleValue proto)
{
// Require unique prototype and name (for reverse lookup)
// TODO: this is yucky - see comment in Deserialize()
ENSURE(proto.isObject() && "A serializable prototype has to be an object!");
JS::RootedObject obj(m_ScriptInterface->GetContext(), &proto.toObject());
if (m_SerializablePrototypes->has(obj) || m_DeserializablePrototypes.find(name) != m_DeserializablePrototypes.end())
{
LOGERROR("RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%s'", (void *)obj.get(), utf8_from_wstring(name));
return;
}
m_SerializablePrototypes->add(m_ScriptInterface->GetContext(), obj, name);
m_DeserializablePrototypes[name] = JS::Heap(obj);
}
private:
static void Trace(JSTracer *trc, void *data)
{
reinterpret_cast(data)->TraceMember(trc);
}
void TraceMember(JSTracer *trc)
{
for (std::pair>& prototype : m_DeserializablePrototypes)
JS_CallHeapObjectTracer(trc, &prototype.second, "CAIWorker::m_DeserializablePrototypes");
for (std::pair>& metadata : m_PlayerMetadata)
JS_CallHeapValueTracer(trc, &metadata.second, "CAIWorker::m_PlayerMetadata");
}
void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out)
{
if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end())
{
// Load and cache the AI player metadata
m_ScriptInterface->ReadJSONFile(path, out);
m_PlayerMetadata[path] = JS::Heap(out);
return;
}
out.set(m_PlayerMetadata[path].get());
}
void PerformComputation()
{
// Deserialize the game state, to pass to the AI's HandleMessage
JSContext* cx = m_ScriptInterface->GetContext();
JSAutoRequest rq(cx);
JS::RootedValue state(cx);
{
PROFILE3("AI compute read state");
m_ScriptInterface->ReadStructuredClone(m_GameState, &state);
m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true);
m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true);
}
// It would be nice to do
// m_ScriptInterface->FreezeObject(state.get(), true);
// to prevent AI scripts accidentally modifying the state and
// affecting other AI scripts they share it with. But the performance
// cost is far too high, so we won't do that.
// If there is a shared component, run it
if (m_HasSharedComponent)
{
PROFILE3("AI run shared component");
m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state);
}
for (size_t i = 0; i < m_Players.size(); ++i)
{
PROFILE3("AI script");
PROFILE2_ATTR("player: %d", m_Players[i]->m_Player);
PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str());
if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent)
m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj);
else
m_Players[i]->Run(state, m_Players[i]->m_Player);
}
}
// Take care to keep this declaration before heap rooted members. Destructors of heap rooted
// members have to be called before the runtime destructor.
shared_ptr m_ScriptRuntime;
shared_ptr m_ScriptInterface;
boost::rand48 m_RNG;
u32 m_TurnNum;
JS::PersistentRootedValue m_EntityTemplates;
bool m_HasLoadedEntityTemplates;
JS::PersistentRootedValue m_TechTemplates;
std::map > m_PlayerMetadata;
std::vector > m_Players; // use shared_ptr just to avoid copying
bool m_HasSharedComponent;
JS::PersistentRootedValue m_SharedAIObj;
std::vector m_Commands;
std::set m_LoadedModules;
shared_ptr m_GameState;
Grid m_PassabilityMap;
JS::PersistentRootedValue m_PassabilityMapVal;
Grid m_TerritoryMap;
JS::PersistentRootedValue m_TerritoryMapVal;
std::map m_NonPathfindingPassClasses;
std::map m_PathfindingPassClasses;
LongPathfinder m_LongPathfinder;
bool m_CommandsComputed;
shared_ptr > m_SerializablePrototypes;
std::map > m_DeserializablePrototypes;
};
/**
* Implementation of ICmpAIManager.
*/
class CCmpAIManager : public ICmpAIManager
{
public:
static void ClassInit(CComponentManager& componentManager)
{
componentManager.SubscribeToMessageType(MT_ProgressiveLoad);
}
DEFAULT_COMPONENT_ALLOCATOR(AIManager)
static std::string GetSchema()
{
return "";
}
virtual void Init(const CParamNode& UNUSED(paramNode))
{
m_TerritoriesDirtyID = 0;
m_JustDeserialized = false;
StartLoadEntityTemplates();
}
virtual void Deinit()
{
}
virtual void Serialize(ISerializer& serialize)
{
serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize());
// Because the AI worker uses its own ScriptInterface, we can't use the
// ISerializer (which was initialised with the simulation ScriptInterface)
// directly. So we'll just grab the ISerializer's stream and write to it
// with an independent serializer.
m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug());
}
virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(paramNode);
u32 numAis;
deserialize.NumberU32_Unbounded("num ais", numAis);
if (numAis > 0)
ForceLoadEntityTemplates();
m_Worker.Deserialize(deserialize.GetStream(), numAis);
m_JustDeserialized = true;
}
virtual void HandleMessage(const CMessage& msg, bool UNUSED(global))
{
switch (msg.GetType())
{
case MT_ProgressiveLoad:
{
const CMessageProgressiveLoad& msgData = static_cast (msg);
*msgData.total += (int)m_TemplateNames.size();
if (*msgData.progressed)
break;
if (ContinueLoadEntityTemplates())
*msgData.progressed = true;
*msgData.progress += (int)m_TemplateLoadedIdx;
break;
}
}
}
virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty)
{
m_Worker.AddPlayer(id, player, difficulty);
// AI players can cheat and see through FoW/SoD, since that greatly simplifies
// their implementation.
// (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD)
CmpPtr cmpRangeManager(GetSystemEntity());
if (cmpRangeManager)
cmpRangeManager->SetLosRevealAll(player, true);
}
virtual void SetRNGSeed(u32 seed)
{
m_Worker.SetRNGSeed(seed);
}
virtual void TryLoadSharedComponent()
{
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
// load the technology templates
- CmpPtr cmpTechTemplateManager(GetSystemEntity());
- ENSURE(cmpTechTemplateManager);
+ CmpPtr cmpDataTemplateManager(GetSystemEntity());
+ ENSURE(cmpDataTemplateManager);
// Get the game state from AIInterface
JS::RootedValue techTemplates(cx);
- cmpTechTemplateManager->GetAllTechs(&techTemplates);
+ cmpDataTemplateManager->GetAllTechs(&techTemplates);
m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates));
m_Worker.TryLoadSharedComponent(true);
}
virtual void RunGamestateInit()
{
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
// We flush events from the initialization so we get a clean state now.
JS::RootedValue state(cx);
cmpAIInterface->GetFullRepresentation(&state, true);
// Get the passability data
Grid dummyGrid;
const Grid* passabilityMap = &dummyGrid;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
Grid dummyGrid2;
const Grid* territoryMap = &dummyGrid2;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID))
{
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
}
LoadPathfinderClasses(state);
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
if (cmpPathfinder)
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks);
}
virtual void StartComputation()
{
PROFILE("AI setup");
ForceLoadEntityTemplates();
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
if (m_Worker.getPlayerSize() == 0)
return;
CmpPtr cmpAIInterface(GetSystemEntity());
ENSURE(cmpAIInterface);
// Get the game state from AIInterface
JS::RootedValue state(cx);
if (m_JustDeserialized)
cmpAIInterface->GetFullRepresentation(&state, false);
else
cmpAIInterface->GetRepresentation(&state);
// Get the passability data
Grid dummyGrid;
const Grid* passabilityMap = &dummyGrid;
CmpPtr cmpPathfinder(GetSystemEntity());
if (cmpPathfinder)
passabilityMap = &cmpPathfinder->GetPassabilityGrid();
GridUpdateInformation dirtinessInformations = cmpPathfinder->GetDirtinessData();
// Get the territory data
// Since getting the territory grid can trigger a recalculation, we check NeedUpdate first
bool territoryMapDirty = false;
Grid dummyGrid2;
const Grid* territoryMap = &dummyGrid2;
CmpPtr cmpTerritoryManager(GetSystemEntity());
if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID))
{
territoryMap = &cmpTerritoryManager->GetTerritoryGrid();
territoryMapDirty = true;
}
LoadPathfinderClasses(state);
std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks;
if (cmpPathfinder)
cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state),
*passabilityMap, dirtinessInformations,
*territoryMap, territoryMapDirty,
nonPathfindingPassClassMasks, pathfindingPassClassMasks);
m_JustDeserialized = false;
}
virtual void PushCommands()
{
std::vector commands;
m_Worker.GetCommands(commands);
CmpPtr cmpCommandQueue(GetSystemEntity());
if (!cmpCommandQueue)
return;
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue clonedCommandVal(cx);
for (size_t i = 0; i < commands.size(); ++i)
{
for (size_t j = 0; j < commands[i].commands.size(); ++j)
{
scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal);
cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal);
}
}
}
private:
std::vector m_TemplateNames;
size_t m_TemplateLoadedIdx;
std::vector > m_Templates;
size_t m_TerritoriesDirtyID;
bool m_JustDeserialized;
void StartLoadEntityTemplates()
{
CmpPtr cmpTemplateManager(GetSystemEntity());
ENSURE(cmpTemplateManager);
m_TemplateNames = cmpTemplateManager->FindAllTemplates(false);
m_TemplateLoadedIdx = 0;
m_Templates.reserve(m_TemplateNames.size());
}
// Tries to load the next entity template. Returns true if we did some work.
bool ContinueLoadEntityTemplates()
{
if (m_TemplateLoadedIdx >= m_TemplateNames.size())
return false;
CmpPtr cmpTemplateManager(GetSystemEntity());
ENSURE(cmpTemplateManager);
const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(m_TemplateNames[m_TemplateLoadedIdx]);
if (node)
m_Templates.emplace_back(m_TemplateNames[m_TemplateLoadedIdx], node);
m_TemplateLoadedIdx++;
// If this was the last template, send the data to the worker
if (m_TemplateLoadedIdx == m_TemplateNames.size())
m_Worker.LoadEntityTemplates(m_Templates);
return true;
}
void ForceLoadEntityTemplates()
{
while (ContinueLoadEntityTemplates())
{
}
}
void LoadPathfinderClasses(JS::HandleValue state)
{
CmpPtr cmpPathfinder(GetSystemEntity());
if (!cmpPathfinder)
return;
ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface();
JSContext* cx = scriptInterface.GetContext();
JSAutoRequest rq(cx);
JS::RootedValue classesVal(cx);
scriptInterface.Eval("({})", &classesVal);
std::map classes;
cmpPathfinder->GetPassabilityClasses(classes);
for (std::map::iterator it = classes.begin(); it != classes.end(); ++it)
scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true);
scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true);
}
CAIWorker m_Worker;
};
REGISTER_COMPONENT_TYPE(AIManager)
Index: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp
===================================================================
--- ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp (revision 18100)
@@ -0,0 +1,39 @@
+/* Copyright (C) 2012 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 "ICmpDataTemplateManager.h"
+
+#include "simulation2/system/InterfaceScripted.h"
+#include "simulation2/scripting/ScriptComponent.h"
+
+BEGIN_INTERFACE_WRAPPER(DataTemplateManager)
+END_INTERFACE_WRAPPER(DataTemplateManager)
+
+class CCmpDataTemplateManagerScripted : public ICmpDataTemplateManager
+{
+public:
+ DEFAULT_SCRIPT_WRAPPER(DataTemplateManagerScripted)
+
+ virtual void GetAllTechs(JS::MutableHandleValue ret)
+ {
+ return m_Script.CallRef("GetAllTechs", ret);
+ }
+};
+
+REGISTER_COMPONENT_SCRIPT_WRAPPER(DataTemplateManagerScripted)
Property changes on: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h
===================================================================
--- ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h (nonexistent)
+++ ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h (revision 18100)
@@ -0,0 +1,38 @@
+/* Copyright (C) 2012 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_ICMPDATATEMPLATEMANAGER
+#define INCLUDED_ICMPDATATEMPLATEMANAGER
+
+#include "simulation2/system/Interface.h"
+
+#include "maths/Fixed.h"
+
+/**
+ * Data template manager interface.
+ * (This interface only includes the functions needed by native code for accessing
+ * json template data, the associated logic is handled in scripts)
+ */
+class ICmpDataTemplateManager : public IComponent
+{
+public:
+ virtual void GetAllTechs(JS::MutableHandleValue ret) = 0;
+
+ DECLARE_INTERFACE_TYPE(DataTemplateManager)
+};
+
+#endif // INCLUDED_ICMPDATATEMPLATEMANAGER
Property changes on: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property