diff --git a/binaries/data/mods/_test.sim/globalscripts/test-global-helper.js b/binaries/data/mods/_test.sim/globalscripts/test-global-helper.js new file mode 100644 index 0000000000..86d2c5d0ab --- /dev/null +++ b/binaries/data/mods/_test.sim/globalscripts/test-global-helper.js @@ -0,0 +1,4 @@ +function GlobalSubtractionHelper(a, b) +{ + return a-b; +} diff --git a/binaries/data/mods/_test.sim/simulation/components/test-global-helper.js b/binaries/data/mods/_test.sim/simulation/components/test-global-helper.js new file mode 100644 index 0000000000..aba376a547 --- /dev/null +++ b/binaries/data/mods/_test.sim/simulation/components/test-global-helper.js @@ -0,0 +1,8 @@ +function TestScript1_GlobalHelper() {} + +TestScript1_GlobalHelper.prototype.GetX = function() +{ + return GlobalSubtractionHelper(1, -1); +}; + +Engine.RegisterComponentType(IID_Test1, "TestScript1_GlobalHelper", TestScript1_GlobalHelper); diff --git a/binaries/data/mods/public/globalscripts/Technologies.js b/binaries/data/mods/public/globalscripts/Technologies.js new file mode 100644 index 0000000000..428ef8f74a --- /dev/null +++ b/binaries/data/mods/public/globalscripts/Technologies.js @@ -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; +} diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index 4ce8c7126d..15a9514bf8 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -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) diff --git a/binaries/data/mods/public/simulation/components/TechnologyManager.js b/binaries/data/mods/public/simulation/components/TechnologyManager.js index c52705ce84..5312ce21ad 100644 --- a/binaries/data/mods/public/simulation/components/TechnologyManager.js +++ b/binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -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); diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js index f72db6cf54..558a95bf90 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -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, diff --git a/source/graphics/MapGenerator.cpp b/source/graphics/MapGenerator.cpp index 518e98e13f..b804165dae 100644 --- a/source/graphics/MapGenerator.cpp +++ b/source/graphics/MapGenerator.cpp @@ -88,7 +88,8 @@ bool CMapGeneratorWorker::Run() m_ScriptInterface->SetCallbackData(static_cast (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("LoadLibrary"); diff --git a/source/gui/GUIManager.cpp b/source/gui/GUIManager.cpp index 2c55661233..bccfc0fe64 100644 --- a/source/gui/GUIManager.cpp +++ b/source/gui/GUIManager.cpp @@ -53,6 +53,8 @@ CGUIManager::CGUIManager(ScriptInterface& scriptInterface) : { ENSURE(ScriptInterface::GetCallbackData(scriptInterface.GetContext()) == NULL); scriptInterface.SetCallbackData(this); + + scriptInterface.LoadGlobalScripts(); } CGUIManager::~CGUIManager() diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index 9a16a0d182..b5b346fe68 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -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) diff --git a/source/scriptinterface/ScriptInterface.h b/source/scriptinterface/ScriptInterface.h index c0118d0fe0..f80f671710 100644 --- a/source/scriptinterface/ScriptInterface.h +++ b/source/scriptinterface/ScriptInterface.h @@ -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)". diff --git a/source/scriptinterface/tests/test_ScriptInterface.h b/source/scriptinterface/tests/test_ScriptInterface.h index ff4ab3edaf..8168306d18 100644 --- a/source/scriptinterface/tests/test_ScriptInterface.h +++ b/source/scriptinterface/tests/test_ScriptInterface.h @@ -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)); diff --git a/source/simulation2/components/CCmpAIManager.cpp b/source/simulation2/components/CCmpAIManager.cpp index 72f946387f..c2470c1a6f 100644 --- a/source/simulation2/components/CCmpAIManager.cpp +++ b/source/simulation2/components/CCmpAIManager.cpp @@ -82,7 +82,8 @@ private: { m_ScriptInterface.SetCallbackData(static_cast (this)); - m_ScriptInterface.ReplaceNondeterministicFunctions(rng); + m_ScriptInterface.ReplaceNondeterministicRNG(rng); + m_ScriptInterface.LoadGlobalScripts(); m_ScriptInterface.RegisterFunction("IncludeModule"); m_ScriptInterface.RegisterFunction("PostCommand"); @@ -269,7 +270,8 @@ public: m_ScriptInterface.SetCallbackData(static_cast (this)); // TODO: ought to seed the RNG (in a network-synchronised way) before we use it - m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG); + m_ScriptInterface.ReplaceNondeterministicRNG(m_RNG); + m_ScriptInterface.LoadGlobalScripts(); } ~CAIWorker() diff --git a/source/simulation2/system/ComponentManager.cpp b/source/simulation2/system/ComponentManager.cpp index c98db5c4f1..09dce48f2d 100644 --- a/source/simulation2/system/ComponentManager.cpp +++ b/source/simulation2/system/ComponentManager.cpp @@ -62,7 +62,8 @@ CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFuncti m_ScriptInterface.SetCallbackData(static_cast (this)); // TODO: ought to seed the RNG (in a network-synchronised way) before we use it - m_ScriptInterface.ReplaceNondeterministicFunctions(m_RNG); + 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 diff --git a/source/simulation2/tests/test_ComponentManager.h b/source/simulation2/tests/test_ComponentManager.h index 215eac724a..a4e66a7eaf 100644 --- a/source/simulation2/tests/test_ComponentManager.h +++ b/source/simulation2/tests/test_ComponentManager.h @@ -287,6 +287,21 @@ public: TS_ASSERT_EQUALS(static_cast (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 (man.QueryInterface(ent1, IID_Test1))->GetX(), 2); + } + void test_script_interface() { CSimContext context;