Implements global tech modification function. Fixes #1358, refs #1520.

Applies tech modifications to template data returned by GuiInterface.
Extends engine to load arbitrary global scripts, separates this from RNG
replacement. Refs #1193.
Loads global scripts for most script contexts for consistency.
Adds simulation tests for global scripts.

This was SVN commit r12056.
This commit is contained in:
historic_bruno 2012-07-03 02:16:45 +00:00
parent db943341a8
commit 99d04e93bb
14 changed files with 195 additions and 111 deletions

View file

@ -0,0 +1,4 @@
function GlobalSubtractionHelper(a, b)
{
return a-b;
}

View file

@ -0,0 +1,8 @@
function TestScript1_GlobalHelper() {}
TestScript1_GlobalHelper.prototype.GetX = function()
{
return GlobalSubtractionHelper(1, -1);
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_GlobalHelper", TestScript1_GlobalHelper);

View file

@ -0,0 +1,67 @@
/**
* This file contains shared logic for applying tech modifications in GUI, AI,
* and simulation scripts. As such it must be fully deterministic and not store
* any global state, but each context should do its own caching as needed.
* Also it cannot directly access the simulation and requires data passed to it.
*/
/**
* Returns modified property value if at least one tech modification is found
* applicable to the given entity template; else, returns its original value.
*
* currentTechModifications: mapping of property names to modification arrays,
* retrieved from the intended player's TechnologyManager.
* entityTemplateData: raw entity template object.
* propertyName: name of the tech modification to apply.
* propertyValue: original value of property to be modified.
*/
function GetTechModifiedProperty(currentTechModifications, entityTemplateData, propertyName, propertyValue)
{
// Get all modifications to this value
var modifications = currentTechModifications[propertyName];
if (!modifications) // no modifications so return the original value
return propertyValue;
// TODO: will we ever need the full template?
// Get the classes which this entity template belongs to
var rawClasses = entityTemplateData.Identity.Classes;
var classes = (rawClasses && "_string" in rawClasses ? rawClasses._string.split(/\s+/) : []);
var retValue = propertyValue;
for (var i in modifications)
{
var modification = modifications[i];
var applies = false;
// See if any of the lists of classes matches this entity
for (var j in modification.affects)
{
var hasAllClasses = true;
// Check each class in affects is present for the entity
for (var k in modification.affects[j])
hasAllClasses = hasAllClasses && (classes.indexOf(modification.affects[j][k]) !== -1);
if (hasAllClasses)
{
applies = true;
break;
}
}
// We found a match, apply the modification
if (applies)
{
// Nothing is cumulative so that ordering doesn't matter as much as possible
if (modification.multiplier)
retValue += (modification.multiplier - 1) * propertyValue;
else if (modification.add)
retValue += modification.add;
else if (modification.replace !== undefined) // This will depend on ordering because there is no choice
retValue = modification.replace;
else
warn("GetTechModifiedProperty: modification format not recognised (modifying " + propertyName + "): " + uneval(modification));
}
}
return retValue;
}

View file

@ -53,13 +53,13 @@ GuiInterface.prototype.GetSimulationState = function(player)
var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player);
// Work out what phase we are in
var cmpTechMan = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
var cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager);
var phase = "";
if (cmpTechMan.IsTechnologyResearched("phase_city"))
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechMan.IsTechnologyResearched("phase_town"))
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechMan.IsTechnologyResearched("phase_village"))
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
// store player ally/enemy data as arrays
@ -85,7 +85,8 @@ GuiInterface.prototype.GetSimulationState = function(player)
"isAlly": allies,
"isEnemy": enemies,
"buildLimits": cmpPlayerBuildLimits.GetLimits(),
"buildCounts": cmpPlayerBuildLimits.GetCounts()
"buildCounts": cmpPlayerBuildLimits.GetCounts(),
"techModifications": cmpTechnologyManager.GetTechModifications()
};
ret.players.push(playerData);
}
@ -133,10 +134,10 @@ GuiInterface.prototype.ClearRenamedEntities = function(player)
GuiInterface.prototype.GetEntityState = function(player, ent)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id
var template = cmpTempMan.GetCurrentTemplateName(ent);
var template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
@ -312,20 +313,23 @@ GuiInterface.prototype.GetEntityState = function(player, ent)
GuiInterface.prototype.GetTemplateData = function(player, name)
{
var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTempMan.GetTemplate(name);
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var template = cmpTemplateManager.GetTemplate(name);
if (!template)
return null;
var ret = {};
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
var techMods = cmpTechnologyManager.GetTechModifications();
if (template.Armour)
{
ret.armour = {
"hack": +template.Armour.Hack,
"pierce": +template.Armour.Pierce,
"crush": +template.Armour.Crush,
"hack": GetTechModifiedProperty(techMods, template, "Armour/Hack", +template.Armour.Hack),
"pierce": GetTechModifiedProperty(techMods, template, "Armour/Pierce", +template.Armour.Pierce),
"crush": GetTechModifiedProperty(techMods, template, "Armour/Crush", +template.Armour.Crush),
};
}
@ -335,9 +339,9 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
for (var type in template.Attack)
{
ret.attack[type] = {
"hack": (+template.Attack[type].Hack || 0),
"pierce": (+template.Attack[type].Pierce || 0),
"crush": (+template.Attack[type].Crush || 0),
"hack": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Hack", +template.Attack[type].Hack || 0),
"pierce": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Pierce", +template.Attack[type].Pierce || 0),
"crush": GetTechModifiedProperty(techMods, template, "Attack/"+type+"/Crush", +template.Attack[type].Crush || 0),
};
}
}
@ -365,12 +369,12 @@ GuiInterface.prototype.GetTemplateData = function(player, name)
if (template.Cost)
{
ret.cost = {};
if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food;
if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood;
if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone;
if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal;
if (template.Cost.Population) ret.cost.population = +template.Cost.Population;
if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus;
if (template.Cost.Resources.food) ret.cost.food = GetTechModifiedProperty(techMods, template, "Cost/Resources/food", +template.Cost.Resources.food);
if (template.Cost.Resources.wood) ret.cost.wood = GetTechModifiedProperty(techMods, template, "Cost/Resources/wood", +template.Cost.Resources.wood);
if (template.Cost.Resources.stone) ret.cost.stone = GetTechModifiedProperty(techMods, template, "Cost/Resources/stone", +template.Cost.Resources.stone);
if (template.Cost.Resources.metal) ret.cost.metal = GetTechModifiedProperty(techMods, template, "Cost/Resources/metal", +template.Cost.Resources.metal);
if (template.Cost.Population) ret.cost.population = GetTechModifiedProperty(techMods, template, "Cost/Population", +template.Cost.Population);
if (template.Cost.PopulationBonus) ret.cost.populationBonus = GetTechModifiedProperty(techMods, template, "Cost/PopulationBonus", +template.Cost.PopulationBonus);
}
if (template.Footprint)
@ -508,23 +512,23 @@ GuiInterface.prototype.GetTechnologyData = function(player, name)
GuiInterface.prototype.IsTechnologyResearched = function(player, tech)
{
var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechMan)
if (!cmpTechnologyManager)
return false;
return cmpTechMan.IsTechnologyResearched(tech);
return cmpTechnologyManager.IsTechnologyResearched(tech);
};
// Checks whether the requirements for this technology have been met
GuiInterface.prototype.CheckTechnologyRequirements = function(player, tech)
{
var cmpTechMan = QueryPlayerIDInterface(player, IID_TechnologyManager);
var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechMan)
if (!cmpTechnologyManager)
return false;
return cmpTechMan.CanResearch(tech);
return cmpTechnologyManager.CanResearch(tech);
};
GuiInterface.prototype.PushNotification = function(notification)

View file

@ -316,62 +316,15 @@ TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, e
this.modificationCache[valueName] = {};
if (!this.modificationCache[valueName][ent])
this.modificationCache[valueName][ent] = this.ApplyModificationsWorker(valueName, curValue, ent);
{
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
var templateName = cmpTemplateManager.GetCurrentTemplateName(ent);
this.modificationCache[valueName][ent] = GetTechModifiedProperty(this.modifications, cmpTemplateManager.GetTemplate(templateName), valueName, curValue);
}
return this.modificationCache[valueName][ent];
}
// The code to actually apply the modification
TechnologyManager.prototype.ApplyModificationsWorker = function(valueName, curValue, ent)
{
// Get all modifications to this value
var modifications = this.modifications[valueName];
if (!modifications) // no modifications so return the original value
return curValue;
// Get the classes which this entity belongs to
var cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
var classes = cmpIdentity.GetClassesList();
var retValue = curValue;
for (var i in modifications)
{
var modification = modifications[i];
var applies = false;
// See if any of the lists of classes matches this entity
for (var j in modification.affects)
{
var hasAllClasses = true;
// Check each class in affects is present for the entity
for (var k in modification.affects[j])
hasAllClasses = hasAllClasses && (classes.indexOf(modification.affects[j][k]) !== -1);
if (hasAllClasses)
{
applies = true;
break;
}
}
// We found a match, apply the modification
if (applies)
{
// Nothing is cumulative so that ordering doesn't matter as much as possible
if (modification.multiplier)
retValue += (modification.multiplier - 1) * curValue;
else if (modification.add)
retValue += modification.add;
else if (modification.replace !== undefined) // This will depend on ordering because there is no choice
retValue = modification.replace;
else
warn("modification format not recognised (modifying " + valueName + "): " + uneval(modification));
}
}
return retValue;
};
// Marks a technology as being currently researched
TechnologyManager.prototype.StartedResearch = function (tech)
{
@ -393,4 +346,10 @@ TechnologyManager.prototype.IsInProgress = function(tech)
return false;
};
// Get helper data for tech modifications
TechnologyManager.prototype.GetTechModifications = function()
{
return this.modifications;
};
Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);

View file

@ -73,6 +73,7 @@ AddMock(100, IID_BuildLimits, {
AddMock(100, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { return false; },
GetTechModifications: function() { return {}; },
});
AddMock(100, IID_StatisticsTracker, {
@ -123,6 +124,7 @@ AddMock(101, IID_BuildLimits, {
AddMock(101, IID_TechnologyManager, {
IsTechnologyResearched: function(tech) { if (tech == "phase_village") return true; else return false; },
GetTechModifications: function() { return {}; },
});
AddMock(101, IID_StatisticsTracker, {
@ -170,6 +172,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
isEnemy: [true, true, true],
buildLimits: {"Foo": 10},
buildCounts: {"Foo": 5},
techModifications: {},
},
{
name: "Player 2",
@ -187,6 +190,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetSimulationState(), {
isEnemy: [false, false, false],
buildLimits: {"Bar": 20},
buildCounts: {"Bar": 0},
techModifications: {},
}
],
circularMap: false,
@ -211,6 +215,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
isEnemy: [true, true, true],
buildLimits: {"Foo": 10},
buildCounts: {"Foo": 5},
techModifications: {},
statistics: {
unitsTrained: 10,
unitsLost: 9,
@ -244,6 +249,7 @@ TS_ASSERT_UNEVAL_EQUALS(cmp.GetExtendedSimulationState(), {
isEnemy: [false, false, false],
buildLimits: {"Bar": 20},
buildCounts: {"Bar": 0},
techModifications: {},
statistics: {
unitsTrained: 10,
unitsLost: 9,

View file

@ -88,7 +88,8 @@ bool CMapGeneratorWorker::Run()
m_ScriptInterface->SetCallbackData(static_cast<void*> (this));
// Replace RNG with a seeded deterministic function
m_ScriptInterface->ReplaceNondeterministicFunctions(m_MapGenRNG);
m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG);
m_ScriptInterface->LoadGlobalScripts();
// Functions for RMS
m_ScriptInterface->RegisterFunction<bool, std::wstring, CMapGeneratorWorker::LoadLibrary>("LoadLibrary");

View file

@ -53,6 +53,8 @@ CGUIManager::CGUIManager(ScriptInterface& scriptInterface) :
{
ENSURE(ScriptInterface::GetCallbackData(scriptInterface.GetContext()) == NULL);
scriptInterface.SetCallbackData(this);
scriptInterface.LoadGlobalScripts();
}
CGUIManager::~CGUIManager()

View file

@ -609,38 +609,44 @@ void* ScriptInterface::GetCallbackData(JSContext* cx)
return JS_GetContextPrivate(cx);
}
void ScriptInterface::ReplaceNondeterministicFunctions(boost::rand48& rng)
bool ScriptInterface::LoadGlobalScripts()
{
jsval math;
if (!JS_GetProperty(m->m_cx, m->m_glob, "Math", &math) || !JSVAL_IS_OBJECT(math))
{
LOGERROR(L"ReplaceNondeterministicFunctions: failed to get Math");
return;
}
// Ignore this failure in tests
if (!g_VFS)
return false;
JSFunction* random = JS_DefineFunction(m->m_cx, JSVAL_TO_OBJECT(math), "random", Math_random, 0,
JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
if (!random)
// Load and execute *.js in the global scripts directory
VfsPaths pathnames;
vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames);
for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it)
{
LOGERROR(L"ReplaceNondeterministicFunctions: failed to replace Math.random");
return;
}
// Store the RNG in a slot which is sort-of-guaranteed to be unused by the JS engine
JS_SetReservedSlot(m->m_cx, JS_GetFunctionObject(random), 0, PRIVATE_TO_JSVAL(&rng));
// Load a script which replaces some of the system dependent trig functions
VfsPath path("globalscripts/Math.js");
// This function is called from tests without the VFS set up,
// so silently fail in that case because every other option seems worse
if (g_VFS && VfsFileExists(path))
{
if (!this->LoadGlobalScriptFile(path))
if (!LoadGlobalScriptFile(*it))
{
LOGERROR(L"ReplaceNondeterministicFunctions: failed to load %ls", path.string().c_str());
return;
LOGERROR(L"LoadGlobalScripts: Failed to load script %ls", it->string().c_str());
return false;
}
}
return true;
}
bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng)
{
jsval math;
if (JS_GetProperty(m->m_cx, m->m_glob, "Math", &math) && JSVAL_IS_OBJECT(math))
{
JSFunction* random = JS_DefineFunction(m->m_cx, JSVAL_TO_OBJECT(math), "random", Math_random, 0,
JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT);
if (random)
{
// Store the RNG in a slot which is sort-of-guaranteed to be unused by the JS engine
if (JS_SetReservedSlot(m->m_cx, JS_GetFunctionObject(random), 0, PRIVATE_TO_JSVAL(&rng)))
return true;
}
}
LOGERROR(L"ReplaceNondeterministicRNG: failed to replace Math.random");
return false;
}
void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs)

View file

@ -98,7 +98,16 @@ public:
JSContext* GetContext() const;
JSRuntime* GetRuntime() const;
void ReplaceNondeterministicFunctions(boost::rand48& rng);
/**
* Load global scripts that most script contexts need,
* located in the /globalscripts directory. VFS must be initialized.
*/
bool LoadGlobalScripts();
/**
* Replace the default JS random number geenrator with a seeded, network-sync'd one.
*/
bool ReplaceNondeterministicRNG(boost::rand48& rng);
/**
* Call a constructor function, equivalent to JS "new ctor(arg)".

View file

@ -116,7 +116,7 @@ public:
TS_ASSERT_DIFFERS(d1, d2);
boost::rand48 rng;
script.ReplaceNondeterministicFunctions(rng);
script.ReplaceNondeterministicRNG(rng);
rng.seed((u64)0);
TS_ASSERT(script.Eval("Math.random()", d1));
TS_ASSERT(script.Eval("Math.random()", d2));

View file

@ -82,7 +82,8 @@ private:
{
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
m_ScriptInterface.ReplaceNondeterministicFunctions(rng);
m_ScriptInterface.ReplaceNondeterministicRNG(rng);
m_ScriptInterface.LoadGlobalScripts();
m_ScriptInterface.RegisterFunction<void, std::wstring, CAIPlayer::IncludeModule>("IncludeModule");
m_ScriptInterface.RegisterFunction<void, CScriptValRooted, CAIPlayer::PostCommand>("PostCommand");
@ -269,7 +270,8 @@ public:
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG);
m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface.LoadGlobalScripts();
}
~CAIWorker()

View file

@ -62,7 +62,8 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
// TODO: ought to seed the RNG (in a network-synchronised way) before we use it
m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG);
m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG);
m_ScriptInterface.LoadGlobalScripts();
// For component script tests, the test system sets up its own scripted implementation of
// these functions, so we skip registering them here in those cases

View file

@ -287,6 +287,21 @@ public:
TS_ASSERT_EQUALS(static_cast<ICmpTest1*> (man.QueryInterface(ent1, IID_Test1))->GetX(), 3);
}
void test_script_global_helper()
{
CSimContext context;
CComponentManager man(context);
man.LoadComponentTypes();
TS_ASSERT(man.LoadScript(L"simulation/components/test-global-helper.js"));
entity_id_t ent1 = 1;
CParamNode noParam;
man.AddComponent(ent1, man.LookupCID("TestScript1_GlobalHelper"), noParam);
TS_ASSERT_EQUALS(static_cast<ICmpTest1*> (man.QueryInterface(ent1, IID_Test1))->GetX(), 2);
}
void test_script_interface()
{
CSimContext context;