Index: ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js (nonexistent) @@ -1,71 +0,0 @@ -/** - * System component which loads the technology and the aura data files - */ -function DataTemplateManager() {} - -DataTemplateManager.prototype.Schema = - ""; - -DataTemplateManager.prototype.Init = function() -{ - this.technologiesPath = "simulation/data/technologies/"; - this.aurasPath = "simulation/data/auras/"; - - this.allTechs = {}; - this.allAuras = {}; - - for (let techName of this.ListAllTechs()) - this.GetTechnologyTemplate(techName); - - for (let auraName of this.ListAllAuras()) - this.GetAuraTemplate(auraName); - - deepfreeze(this.allTechs); - deepfreeze(this.allAuras); -}; - -DataTemplateManager.prototype.GetTechnologyTemplate = function(template) -{ - if (!this.allTechs[template]) - { - this.allTechs[template] = Engine.ReadJSONFile(this.technologiesPath + 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(this.aurasPath + template + ".json"); - if (!this.allAuras[template]) - error("Failed to load aura \"" + template + "\""); - } - - return this.allAuras[template]; -}; - -DataTemplateManager.prototype.ListAllTechs = function() -{ - return listFiles(this.technologiesPath, ".json", true); -}; - -DataTemplateManager.prototype.ListAllAuras = function() -{ - return listFiles(this.aurasPath, ".json", true); -}; - -DataTemplateManager.prototype.GetAllTechs = function() -{ - return this.allTechs; -}; - -DataTemplateManager.prototype.TechnologyExists = function(template) -{ - return !!this.allTechs[template]; -}; - -Engine.RegisterSystemComponentType(IID_DataTemplateManager, "DataTemplateManager", DataTemplateManager); Property changes on: ps/trunk/binaries/data/mods/public/simulation/components/DataTemplateManager.js ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp (revision 20736) +++ ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp (nonexistent) @@ -1,39 +0,0 @@ -/* Copyright (C) 2017 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) - { - m_Script.CallRef("GetAllTechs", ret); - } -}; - -REGISTER_COMPONENT_SCRIPT_WRAPPER(DataTemplateManagerScripted) Property changes on: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.cpp ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpDataTemplateManager.h (revision 20736) +++ ps/trunk/source/simulation2/components/ICmpDataTemplateManager.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_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 ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/globalscripts/ModificationTemplates.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/ModificationTemplates.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/globalscripts/ModificationTemplates.js (revision 20737) @@ -0,0 +1,45 @@ +/** + * @file This provides a cache for Aura and Technology templates. + * They may not be serialized, otherwise rejoined clients would refer + * to different objects, triggering an Out-of-sync error. + */ +function ModificationTemplates(path) +{ + let suffix = ".json"; + + this.names = deepfreeze(listFiles(path, suffix, true)); + + this.templates = {}; + + for (let name of this.names) + this.templates[name] = Engine.ReadJSONFile(path + name + suffix); + + deepfreeze(this.templates); +} + +ModificationTemplates.prototype.GetNames = function() +{ + return this.names; +}; + +ModificationTemplates.prototype.Has = function(name) +{ + return this.names.indexOf(name) != -1; +}; + +ModificationTemplates.prototype.Get = function(name) +{ + return this.templates[name]; +}; + +ModificationTemplates.prototype.GetAll = function() +{ + return this.templates; +}; + + +function LoadModificationTemplates() +{ + global.AuraTemplates = new ModificationTemplates("simulation/data/auras/"); + global.TechnologyTemplates = new ModificationTemplates("simulation/data/technologies/"); +} Property changes on: ps/trunk/binaries/data/mods/public/globalscripts/ModificationTemplates.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 20737) @@ -1,942 +1,942 @@ var API3 = function(m) { // defines a template. m.Template = m.Class({ "_init": function(sharedAI, templateName, template) { this._templateName = templateName; this._template = template; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; this._tpCache = new Map(); }, // helper function to return a template value, optionally adjusting for tech. // TODO: there's no support for "_string" values here. "get": function(string) { let value = this._template; if (this._entityModif && this._entityModif.has(string)) return this._entityModif.get(string); else if (this._templateModif) { let owner = this._entity ? this._entity.owner : PlayerID; if (this._templateModif[owner] && this._templateModif[owner].has(string)) return this._templateModif[owner].get(string); } if (!this._tpCache.has(string)) { let args = string.split("/"); for (let arg of args) { if (value[arg]) value = value[arg]; else { value = undefined; break; } } this._tpCache.set(string, value); } return this._tpCache.get(string); }, "templateName": function() { return this._templateName; }, "genericName": function() { return this.get("Identity/GenericName"); }, "civ": function() { return this.get("Identity/Civ"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; return classes && classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); let classes = this._classes; if (!classes) return false; for (let cls of array) if (classes.indexOf(cls) == -1) return false; return true; }, "requiredTech": function() { return this.get("Identity/RequiredTechnology"); }, "available": function(gameState) { let techRequired = this.requiredTech(); if (!techRequired) return true; return gameState.isResearched(techRequired); }, // specifically "phase": function() { let techRequired = this.requiredTech(); if (!techRequired) return 0; if (techRequired == "phase_village") return 1; if (techRequired == "phase_town") return 2; if (techRequired == "phase_city") return 3; if (techRequired.startsWith("phase_")) return 4; return 0; }, "cost": function(productionQueue) { if (!this.get("Cost")) return undefined; let ret = {}; for (let type in this.get("Cost/Resources")) ret[type] = +this.get("Cost/Resources/" + type); return ret; }, "costSum": function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("ProductionQueue/TechCostMultiplier/"+type) || 1); }, /** * Returns { "max": max, "min": min } or undefined if no obstruction. * max: radius of the outer circle surrounding this entity's obstruction shape * min: radius of the inner circle */ "obstructionRadius": function() { if (!this.get("Obstruction")) return undefined; if (this.get("Obstruction/Static")) { let w = +this.get("Obstruction/Static/@width"); let h = +this.get("Obstruction/Static/@depth"); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } if (this.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); return { "max": r, "min": r }; } let right = this.get("Obstruction/Obstructions/Right"); let left = this.get("Obstruction/Obstructions/Left"); if (left && right) { let w = +right["@x"] + right["@width"]/2 - left["@x"] + left["@width"]/2; let h = Math.max(+right["@z"] + right["@depth"]/2, +left["@z"] + left["@depth"]/2) - Math.min(+right["@z"] - right["@depth"]/2, +left["@z"] - left["@depth"]/2); return { "max": Math.sqrt(w*w + h*h) / 2, "min": Math.min(h, w) / 2 }; } return { "max": 0, "min": 0 }; // Units have currently no obstructions }, /** * Returns the radius of a circle surrounding this entity's footprint. */ "footprintRadius": function() { if (!this.get("Footprint")) return undefined; if (this.get("Footprint/Square")) { let w = +this.get("Footprint/Square/@width"); let h = +this.get("Footprint/Square/@depth"); return Math.sqrt(w*w + h*h) / 2; } if (this.get("Footprint/Circle")) return +this.get("Footprint/Circle/@radius"); return 0; // this should never happen }, "maxHitpoints": function() { return +(this.get("Health/Max") || 0); }, "isHealable": function() { if (this.get("Health") !== undefined) return this.get("Health/Unhealable") !== "true"; return false; }, "isRepairable": function() { return this.get("Repairable") !== undefined; }, "getPopulationBonus": function() { return +this.get("Cost/PopulationBonus"); }, "armourStrengths": function() { if (!this.get("Armour")) return undefined; return { "Hack": +this.get("Armour/Hack"), "Pierce": +this.get("Armour/Pierce"), "Crush": +this.get("Armour/Crush") }; }, "attackTypes": function() { if (!this.get("Attack")) return undefined; let ret = []; for (let type in this.get("Attack")) ret.push(type); return ret; }, "attackRange": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { max: +this.get("Attack/" + type +"/MaxRange"), min: +(this.get("Attack/" + type +"/MinRange") || 0) }; }, "attackStrengths": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { "Hack": +(this.get("Attack/" + type + "/Hack") || 0), "Pierce": +(this.get("Attack/" + type + "/Pierce") || 0), "Crush": +(this.get("Attack/" + type + "/Crush") || 0) }; }, "captureStrength": function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Value") || 0; }, "attackTimes": function(type) { if (!this.get("Attack/" + type +"")) return undefined; return { prepare: +(this.get("Attack/" + type + "/PrepareTime") || 0), repeat: +(this.get("Attack/" + type + "/RepeatTime") || 1000) }; }, // returns the classes this templates counters: // Return type is [ [-neededClasses- , multiplier], … ]. "getCounteredClasses": function() { if (!this.get("Attack")) return undefined; let Classes = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses" + b +"/Multiplier")]); } } return Classes; }, // returns true if the entity counters those classes. // TODO: refine using the multiplier "countersClasses": function(classes) { if (!this.get("Attack")) return false; let mcounter = []; for (let type in this.get("Attack")) { let bonuses = this.get("Attack/" + type + "/Bonuses"); if (!bonuses) continue; for (let b in bonuses) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (bonusClasses) mcounter.concat(bonusClasses.split(" ")); } } for (let i in classes) if (mcounter.indexOf(classes[i]) != -1) return true; return false; }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; if (this.get("Attack/" + type + "/Bonuses")) { for (let b in this.get("Attack/" + type + "/Bonuses")) { let bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes"); if (!bonusClasses) continue; for (let bcl of bonusClasses.split(" ")) if (bcl == againstClass) return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier"); } } return 1; }, "buildableEntities": function(civ) { let templates = this.get("Builder/Entities/_string"); if (!templates) return []; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "trainableEntities": function(civ) { let templates = this.get("ProductionQueue/Entities/_string"); if (!templates) return undefined; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "researchableTechs": function(gameState, civ) { let templates = this.get("ProductionQueue/Technologies/_string"); if (!templates) return undefined; let techs = templates.split(/\s+/); for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", civ); - techs[i] = gameState.techTemplates[civTech] ? + techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } return techs; }, "resourceSupplyType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); return { "generic": type, "specific": subtype }; }, // will return either "food", "wood", "stone", "metal" and not treasure. "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; let [type, subtype] = this.get("ResourceSupply/Type").split('.'); if (type == "treasure") return subtype; return type; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Amount"); }, "maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); }, "resourceGatherRates": function() { if (!this.get("ResourceGatherer")) return undefined; let ret = {}; let baseSpeed = +this.get("ResourceGatherer/BaseSpeed"); for (let r in this.get("ResourceGatherer/Rates")) ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed; return ret; }, "resourceDropsiteTypes": function() { if (!this.get("ResourceDropsite")) return undefined; let types = this.get("ResourceDropsite/Types"); return types ? types.split(/\s+/) : []; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); }, "getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); }, "getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); }, "getGarrisonArrowClasses": function() { if (!this.get("BuildingAI")) return undefined; return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/); }, "buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); }, "promotion": function() { return this.get("Promotion/Entity"); }, /** * Returns whether this is an animal that is too difficult to hunt. */ "isHuntable": function() { if(!this.get("ResourceSupply/KillBeforeGather")) return false; // do not hunt retaliating animals (animals without UnitAI are dead animals) let behaviour = this.get("UnitAI/NaturalBehaviour"); return !behaviour || (behaviour != "violent" && behaviour != "aggressive" && behaviour != "defensive"); }, "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); }, "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); }, "buildTime": function(productionQueue) { let time = +this.get("Cost/BuildTime"); if (productionQueue) time *= productionQueue.techCostMultiplier("time"); return time; }, "buildCategory": function() { return this.get("BuildRestrictions/Category"); }, "buildDistance": function() { let distance = this.get("BuildRestrictions/Distance"); if (!distance) return undefined; let ret = {}; for (let key in distance) ret[key] = this.get("BuildRestrictions/Distance/" + key); return ret; }, "buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); }, "buildTerritories": function() { if (!this.get("BuildRestrictions") || !this.get("BuildRestrictions/Territory")) return undefined; return this.get("BuildRestrictions/Territory").split(/\s+/); }, "hasBuildTerritory": function(territory) { let territories = this.buildTerritories(); return territories && territories.indexOf(territory) != -1; }, "hasTerritoryInfluence": function() { return this.get("TerritoryInfluence") !== undefined; }, "hasDefensiveFire": function() { if (!this.get("Attack/Ranged")) return false; return this.getDefaultArrow() || this.getArrowMultiplier(); }, "territoryInfluenceRadius": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Radius"); return -1; }, "territoryInfluenceWeight": function() { if (this.get("TerritoryInfluence") !== undefined) return +this.get("TerritoryInfluence/Weight"); return -1; }, "territoryDecayRate": function() { return +(this.get("TerritoryDecay/DecayRate") || 0); }, "defaultRegenRate": function() { return +(this.get("Capturable/RegenRate") || 0); }, "garrisonRegenRate": function() { return +(this.get("Capturable/GarrisonRegenRate") || 0); }, "visionRange": function() { return +this.get("Vision/Range"); }, "gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); }, "isBuilder": function() { return this.get("Builder") !== undefined; }, "isGatherer": function() { return this.get("ResourceGatherer") !== undefined; }, "canGather": function(type) { let gatherRates = this.get("ResourceGatherer/Rates"); if (!gatherRates) return false; for (let r in gatherRates) if (r.split('.')[0] === type) return true; return false; }, "isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; }, /** * returns true if the tempalte can capture the given target entity * if no target is given, returns true if the template has the Capture attack */ "canCapture": function(target) { if (!this.get("Attack/Capture")) return false; if (!target) return true; if (!target.get("Capturable")) return false; let restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string"); return !restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, }); // defines an entity, with a super Template. // also redefines several of the template functions where the only change is applying aura and tech modifications. m.Entity = m.Class({ "_super": m.Template, "_init": function(sharedAI, entity) { this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template)); this._entity = entity; this._ai = sharedAI; // save a reference to the template tech modifications if (!sharedAI._templatesModifications[this._templateName]) sharedAI._templatesModifications[this._templateName] = {}; this._templateModif = sharedAI._templatesModifications[this._templateName]; // save a reference to the entity tech/aura modifications if (!sharedAI._entitiesModifications.has(entity.id)) sharedAI._entitiesModifications.set(entity.id, new Map()); this._entityModif = sharedAI._entitiesModifications.get(entity.id); }, "toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; }, "id": function() { return this._entity.id; }, /** * Returns extra data that the AI scripts have associated with this entity, * for arbitrary local annotations. * (This data should not be shared with any other AI scripts.) */ "getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); }, /** * Sets extra data to be associated with this entity. */ "setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); }, "deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; }, "deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); }, "position": function() { return this._entity.position; }, "angle": function() { return this._entity.angle; }, "isIdle": function() { if (typeof this._entity.idle === "undefined") return undefined; return this._entity.idle; }, "getStance": function() { return this._entity.stance !== undefined ? this._entity.stance : undefined; }, "unitAIState": function() { return this._entity.unitAIState !== undefined ? this._entity.unitAIState : undefined; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData !== undefined ? this._entity.unitAIOrderData : undefined; }, "hitpoints": function() { return this._entity.hitpoints !== undefined ? this._entity.hitpoints : undefined; }, "isHurt": function() { return this.hitpoints() < this.maxHitpoints(); }, "healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); }, "needsHeal": function() { return this.isHurt() && this.isHealable(); }, "needsRepair": function() { return this.isHurt() && this.isRepairable(); }, "decaying": function() { return this._entity.decaying !== undefined ? this._entity.decaying : undefined; }, "capturePoints": function() {return this._entity.capturePoints !== undefined ? this._entity.capturePoints : undefined; }, "isSharedDropsite": function() { return this._entity.sharedDropsite === true; }, /** * Returns the current training queue state, of the form * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ] */ "trainingQueue": function() { let queue = this._entity.trainingQueue; return queue; }, "trainingQueueTime": function() { let queue = this._entity.trainingQueue; if (!queue) return undefined; let time = 0; for (let item of queue) time += item.timeRemaining; return time/1000; }, "foundationProgress": function() { if (this._entity.foundationProgress === undefined) return undefined; return this._entity.foundationProgress; }, "getBuilders": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return []; return this._entity.foundationBuilders; }, "getBuildersNb": function() { if (this._entity.foundationProgress === undefined) return undefined; if (this._entity.foundationBuilders === undefined) return 0; return this._entity.foundationBuilders.length; }, "owner": function() { return this._entity.owner; }, "isOwn": function(player) { if (typeof this._entity.owner === "undefined") return false; return this._entity.owner === player; }, "resourceSupplyAmount": function() { if (this._entity.resourceSupplyAmount === undefined) return undefined; return this._entity.resourceSupplyAmount; }, "resourceSupplyNumGatherers": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this._entity.resourceSupplyNumGatherers; return undefined; }, "isFull": function() { if (this._entity.resourceSupplyNumGatherers !== undefined) return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; return undefined; }, "resourceCarrying": function() { if (this._entity.resourceCarrying === undefined) return undefined; return this._entity.resourceCarrying; }, "currentGatherRate": function() { // returns the gather rate for the current target if applicable. if (!this.get("ResourceGatherer")) return undefined; if (this.unitAIOrderData().length && (this.unitAIState().split(".")[1] == "GATHER" || this.unitAIState().split(".")[1] == "RETURNRESOURCE")) { let res; // this is an abuse of "_ai" but it works. if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[0].target); else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined) res = this._ai._entities.get(this.unitAIOrderData()[1].target); if (!res) return 0; let type = res.resourceSupplyType(); if (!type) return 0; if (type.generic == "treasure") return 1000; let tstring = type.generic + "." + type.specific; let rate = +this.get("ResourceGatherer/BaseSpeed"); rate *= +this.get("ResourceGatherer/Rates/" +tstring); if (rate) return rate; return 0; } return undefined; }, "garrisoned": function() { return this._entity.garrisoned; }, "canGarrisonInside": function() { return this._entity.garrisoned.length < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { if (!this.get("Attack")) return false; for (let type in this.get("Attack")) { if (type == "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, "move": function(x, z, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; }, "moveToRange": function(x, z, min, max, queued = false) { Engine.PostCommand(PlayerID,{"type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued }); return this; }, "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance, queued = false) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID,{"type": "stance", "entities": [this.id()], "name" : stance, "queued": queued }); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID,{"type": "stop", "entities": [this.id()], "queued": false}); }, "unload": function(id) { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload", "garrisonHolder": this.id(), "entities": [id]}); return this; }, // Unloads all owned units, don't unload allies "unloadAll": function() { if (!this.get("GarrisonHolder")) return undefined; Engine.PostCommand(PlayerID,{"type": "unload-all-by-owner", "garrisonHolders": [this.id()]}); return this; }, "garrison": function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "garrison", "entities": [this.id()], "target": target.id(),"queued": queued}); return this; }, "attack": function(unitId, allowCapture = true, queued = false) { Engine.PostCommand(PlayerID,{"type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued}); return this; }, // moveApart from a point in the opposite direction with a distance dist "moveApart": function(point, dist) { if (this.position() !== undefined) { let direction = [this.position()[0] - point[0], this.position()[1] - point[1]]; let norm = m.VectorDistance(point, this.position()); if (norm === 0) direction = [1, 0]; else { direction[0] /= norm; direction[1] /= norm; } Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false}); } return this; }, // Flees from a unit in the opposite direction. "flee": function(unitToFleeFrom) { if (this.position() !== undefined && unitToFleeFrom.position() !== undefined) { let FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0], this.position()[1] - unitToFleeFrom.position()[1]]; let dist = m.VectorDistance(unitToFleeFrom.position(), this.position() ); FleeDirection[0] = 40 * FleeDirection[0]/dist; FleeDirection[1] = 40 * FleeDirection[1]/dist; Engine.PostCommand(PlayerID,{"type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false}); } return this; }, "gather": function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, "repair": function(target, autocontinue = false, queued = false) { Engine.PostCommand(PlayerID,{"type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued}); return this; }, "returnResources": function(target, queued = false) { Engine.PostCommand(PlayerID,{"type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued}); return this; }, "destroy": function() { Engine.PostCommand(PlayerID,{"type": "delete-entities", "entities": [this.id()] }); return this; }, "barter": function(buyType, sellType, amount) { Engine.PostCommand(PlayerID,{"type": "barter", "sell" : sellType, "buy" : buyType, "amount" : amount }); return this; }, "tradeRoute": function(target, source) { Engine.PostCommand(PlayerID,{"type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false }); return this; }, "setRallyPoint": function(target, command) { let data = {"command": command, "target": target.id()}; Engine.PostCommand(PlayerID, {"type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data}); return this; }, "unsetRallyPoint": function() { Engine.PostCommand(PlayerID, {"type": "unset-rallypoint", "entities": [this.id()]}); return this; }, "train": function(civ, type, count, metadata, promotedTypes) { let trainable = this.trainableEntities(civ); if (!trainable) { error("Called train("+type+", "+count+") on non-training entity "+this); return this; } if (trainable.indexOf(type) == -1) { error("Called train("+type+", "+count+") on entity "+this+" which can't train that"); return this; } Engine.PostCommand(PlayerID,{ "type": "train", "entities": [this.id()], "template": type, "count": count, "metadata": metadata, "promoted": promotedTypes }); return this; }, "construct": function(template, x, z, angle, metadata) { // TODO: verify this unit can construct this, just for internal // sanity-checking and error reporting Engine.PostCommand(PlayerID,{ "type": "construct", "entities": [this.id()], "template": template, "x": x, "z": z, "angle": angle, "autorepair": false, "autocontinue": false, "queued": false, "metadata" : metadata // can be undefined }); return this; }, "research": function(template) { Engine.PostCommand(PlayerID,{ "type": "research", "entity": this.id(), "template": template }); return this; }, "stopProduction": function(id) { Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": id }); return this; }, "stopAllProduction": function(percentToStopAt) { let queue = this._entity.trainingQueue; if (!queue) return true; // no queue, so technically we stopped all production. for (let item of queue) if (item.progress < percentToStopAt) Engine.PostCommand(PlayerID,{ "type": "stop-production", "entity": this.id(), "id": item.id }); return this; }, "guard": function(target, queued = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued }); return this; }, "removeGuard": function() { Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] }); return this; } }); return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/gamestate.js (revision 20737) @@ -1,943 +1,943 @@ var API3 = function(m) { /** * Provides an API for the rest of the AI scripts to query the world state at a * higher level than the raw data. */ m.GameState = function() { this.ai = null; // must be updated by the AIs. }; -m.GameState.prototype.init = function(SharedScript, state, player) { +m.GameState.prototype.init = function(SharedScript, state, player) +{ this.sharedScript = SharedScript; this.EntCollecNames = SharedScript._entityCollectionsName; this.timeElapsed = SharedScript.timeElapsed; this.circularMap = SharedScript.circularMap; this.templates = SharedScript._templates; - this.techTemplates = SharedScript._techTemplates; this.entities = SharedScript.entities; this.player = player; this.playerData = SharedScript.playersData[this.player]; this.gameType = SharedScript.gameType; this.alliedVictory = SharedScript.alliedVictory; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; // get the list of possible phases for this civ: // we assume all of them are researchable from the civil centre this.phases = [{ name: "phase_village" }, { name: "phase_town" }, { name: "phase_city" }]; let cctemplate = this.getTemplate(this.applyCiv("structures/{civ}_civil_centre")); if (!cctemplate) return; let civ = this.getPlayerCiv(); let techs = cctemplate.researchableTechs(this, civ); for (let phase of this.phases) { phase.requirements = []; let k = techs.indexOf(phase.name); if (k !== -1) { let reqs = DeriveTechnologyRequirements(this.getTemplate(techs[k])._template, civ); if (reqs) { phase.requirements = reqs; continue; } } for (let tech of techs) { let template = this.getTemplate(tech)._template; if (template.replaces && template.replaces.indexOf(phase.name) != -1) { let reqs = DeriveTechnologyRequirements(template, civ); if (reqs) { phase.name = tech; phase.requirements = reqs; break; } } } } // Then check if this mod has an additionnal phase for (let tech of techs) { let template = this.getTemplate(tech)._template; if (!template.supersedes || template.supersedes != this.phases[2].name) continue; let reqs = DeriveTechnologyRequirements(template, civ); if (reqs) this.phases.push({ "name": tech, "requirements": reqs }); break; } }; m.GameState.prototype.update = function(SharedScript) { this.timeElapsed = SharedScript.timeElapsed; this.playerData = SharedScript.playersData[this.player]; this.ceasefireActive = SharedScript.ceasefireActive; this.ceasefireTimeRemaining = SharedScript.ceasefireTimeRemaining; }; m.GameState.prototype.updatingCollection = function(id, filter, parentCollection) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID return this.updatingGlobalCollection(gid, filter, parentCollection); }; m.GameState.prototype.destroyCollection = function(id) { let gid = "player-" + this.player + "-" + id; // automatically add the player ID this.destroyGlobalCollection(gid); }; m.GameState.prototype.updatingGlobalCollection = function(gid, filter, parentCollection) { if (this.EntCollecNames.has(gid)) return this.EntCollecNames.get(gid); let collection = parentCollection ? parentCollection.filter(filter) : this.entities.filter(filter); collection.registerUpdates(); this.EntCollecNames.set(gid, collection); return collection; }; m.GameState.prototype.destroyGlobalCollection = function(gid) { if (!this.EntCollecNames.has(gid)) return; this.sharedScript.removeUpdatingEntityCollection(this.EntCollecNames.get(gid)); this.EntCollecNames.delete(gid); }; /** * Reset the entities collections which depend on diplomacy */ m.GameState.prototype.resetOnDiplomacyChanged = function() { for (let name of this.EntCollecNames.keys()) if (name.startsWith("player-" + this.player + "-diplo")) this.destroyGlobalCollection(name); }; m.GameState.prototype.getTimeElapsed = function() { return this.timeElapsed; }; m.GameState.prototype.getBarterPrices = function() { return this.playerData.barterPrices; }; m.GameState.prototype.getGameType = function() { return this.gameType; }; m.GameState.prototype.getAlliedVictory = function() { return this.alliedVictory; }; m.GameState.prototype.isCeasefireActive = function() { return this.ceasefireActive; }; m.GameState.prototype.getTemplate = function(type) { - if (this.techTemplates[type] !== undefined) - return new m.Technology(this.techTemplates, type); + if (TechnologyTemplates.Has(type)) + return new m.Technology(type); if (this.templates[type] === undefined) this.sharedScript.GetTemplate(type); return this.templates[type] ? new m.Template(this.sharedScript, type, this.templates[type]) : null; }; /** Return the template of the structure built from this foundation */ m.GameState.prototype.getBuiltTemplate = function(foundationName) { if (!foundationName.startsWith("foundation|")) { warn("Foundation " + foundationName + " not recognised as a foundation."); return null; } return this.getTemplate(foundationName.substr(11)); }; m.GameState.prototype.applyCiv = function(str) { return str.replace(/\{civ\}/g, this.playerData.civ); }; m.GameState.prototype.getPlayerCiv = function(player) { return player !== undefined ? this.sharedScript.playersData[player].civ : this.playerData.civ; }; m.GameState.prototype.currentPhase = function() { for (let i = this.phases.length; i > 0; --i) if (this.isResearched(this.phases[i-1].name)) return i; return 0; }; m.GameState.prototype.getNumberOfPhases = function() { return this.phases.length; }; m.GameState.prototype.getPhaseName = function(i) { return this.phases[i-1] ? this.phases[i-1].name : undefined; }; m.GameState.prototype.getPhaseEntityRequirements = function(i) { let entityReqs = []; for (let requirement of this.phases[i-1].requirements) { if (!requirement.entities) continue; for (let entity of requirement.entities) if (entity.check == "count") entityReqs.push({ "class": entity.class, "count": entity.number }); } return entityReqs; }; m.GameState.prototype.isResearched = function(template) { return this.playerData.researchedTechs.has(template); }; /** true if started or queued */ m.GameState.prototype.isResearching = function(template) { return this.playerData.researchStarted.has(template) || this.playerData.researchQueued.has(template); }; /** this is an "in-absolute" check that doesn't check if we have a building to research from. */ m.GameState.prototype.canResearch = function(techTemplateName, noRequirementCheck) { if (this.playerData.disabledTechnologies[techTemplateName]) return false; let template = this.getTemplate(techTemplateName); if (!template) return false; // researching or already researched: NOO. if (this.playerData.researchQueued.has(techTemplateName) || this.playerData.researchStarted.has(techTemplateName) || this.playerData.researchedTechs.has(techTemplateName)) return false; if (noRequirementCheck) return true; // if this is a pair, we must check that the pair tech is not being researched if (template.pair()) { let other = template.pairedWith(); if (this.playerData.researchQueued.has(other) || this.playerData.researchStarted.has(other) || this.playerData.researchedTechs.has(other)) return false; } return this.checkTechRequirements(template.requirements(this.playerData.civ)); }; /** * Private function for checking a set of requirements is met. * Basically copies TechnologyManager, but compares against * variables only available within the AI */ m.GameState.prototype.checkTechRequirements = function(reqs) { if (!reqs) return false; if (!reqs.length) return true; function doesEntitySpecPass(entity) { switch (entity.check) { case "count": if (!this.playerData.classCounts[entity.class] || this.playerData.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.playerData.typeCountsByClass[entity.class] || Object.keys(this.playerData.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; } return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(tech => this.playerData.researchedTechs.has(tech)); case "entities": return req[type].every(doesEntitySpecPass, this); } return false; }); }); }; m.GameState.prototype.getPassabilityMap = function() { return this.sharedScript.passabilityMap; }; m.GameState.prototype.getPassabilityClassMask = function(name) { if (!this.sharedScript.passabilityClasses[name]) error("Tried to use invalid passability class name '" + name + "'"); return this.sharedScript.passabilityClasses[name]; }; m.GameState.prototype.getResources = function() { return new m.Resources(this.playerData.resourceCounts); }; m.GameState.prototype.getPopulation = function() { return this.playerData.popCount; }; m.GameState.prototype.getPopulationLimit = function() { return this.playerData.popLimit; }; m.GameState.prototype.getPopulationMax = function() { return this.playerData.popMax; }; m.GameState.prototype.getPlayerID = function() { return this.player; }; m.GameState.prototype.hasAllies = function() { for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasEnemies = function() { for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i] && +i !== 0 && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.hasNeutrals = function() { for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i] && this.sharedScript.playersData[i].state !== "defeated") return true; return false; }; m.GameState.prototype.isPlayerNeutral = function(id) { return this.playerData.isNeutral[id]; }; m.GameState.prototype.isPlayerAlly = function(id) { return this.playerData.isAlly[id]; }; m.GameState.prototype.isPlayerMutualAlly = function(id) { return this.playerData.isMutualAlly[id]; }; m.GameState.prototype.isPlayerEnemy = function(id) { return this.playerData.isEnemy[id]; }; /** Return the number of players currently enemies, not including gaia */ m.GameState.prototype.getNumPlayerEnemies = function() { let num = 0; for (let i = 1; i < this.playerData.isEnemy.length; ++i) if (this.playerData.isEnemy[i] && this.sharedScript.playersData[i].state != "defeated") ++num; return num; }; m.GameState.prototype.getEnemies = function() { let ret = []; for (let i in this.playerData.isEnemy) if (this.playerData.isEnemy[i]) ret.push(+i); return ret; }; m.GameState.prototype.getNeutrals = function() { let ret = []; for (let i in this.playerData.isNeutral) if (this.playerData.isNeutral[i]) ret.push(+i); return ret; }; m.GameState.prototype.getAllies = function() { let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i]) ret.push(+i); return ret; }; m.GameState.prototype.getExclusiveAllies = function() { // Player is not included let ret = []; for (let i in this.playerData.isAlly) if (this.playerData.isAlly[i] && +i !== this.player) ret.push(+i); return ret; }; m.GameState.prototype.getMutualAllies = function() { let ret = []; for (let i in this.playerData.isMutualAlly) if (this.playerData.isMutualAlly[i] && this.sharedScript.playersData[i].isMutualAlly[this.player]) ret.push(+i); return ret; }; m.GameState.prototype.isEntityAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()]; }; m.GameState.prototype.isEntityExclusiveAlly = function(ent) { if (!ent) return false; return this.playerData.isAlly[ent.owner()] && ent.owner() !== this.player; }; m.GameState.prototype.isEntityEnemy = function(ent) { if (!ent) return false; return this.playerData.isEnemy[ent.owner()]; }; m.GameState.prototype.isEntityOwn = function(ent) { if (!ent) return false; return ent.owner() === this.player; }; m.GameState.prototype.getEntityById = function(id) { if (this.entities._entities.has(+id)) return this.entities._entities.get(+id); return undefined; }; m.GameState.prototype.getEntities = function(id) { if (id === undefined) return this.entities; return this.updatingGlobalCollection("player-" + id + "-entities", m.Filters.byOwner(id)); }; m.GameState.prototype.getStructures = function() { return this.updatingGlobalCollection("structures", m.Filters.byClass("Structure"), this.entities); }; m.GameState.prototype.getOwnEntities = function() { return this.updatingGlobalCollection("player-" + this.player + "-entities", m.Filters.byOwner(this.player)); }; m.GameState.prototype.getOwnStructures = function() { return this.updatingGlobalCollection("player-" + this.player + "-structures", m.Filters.byClass("Structure"), this.getOwnEntities()); }; m.GameState.prototype.getOwnUnits = function() { return this.updatingGlobalCollection("player-" + this.player + "-units", m.Filters.byClass("Unit"), this.getOwnEntities()); }; m.GameState.prototype.getAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getAllies())); }; m.GameState.prototype.getExclusiveAllyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getExclusiveAllies())); }; m.GameState.prototype.getAllyStructures = function(allyID) { if (allyID == undefined) return this.updatingCollection("diplo-ally-structures", m.Filters.byOwners(this.getAllies()), this.getStructures()); return this.updatingGlobalCollection("player-" + allyID + "-structures", m.Filters.byOwner(allyID), this.getStructures()); }; m.GameState.prototype.getNeutralStructures = function() { return this.getStructures().filter(m.Filters.byOwners(this.getNeutrals())); }; m.GameState.prototype.getEnemyEntities = function() { return this.entities.filter(m.Filters.byOwners(this.getEnemies())); }; m.GameState.prototype.getEnemyStructures = function(enemyID) { if (enemyID === undefined) return this.updatingCollection("diplo-enemy-structures", m.Filters.byOwners(this.getEnemies()), this.getStructures()); return this.updatingGlobalCollection("player-" + enemyID + "-structures", m.Filters.byOwner(enemyID), this.getStructures()); }; m.GameState.prototype.getEnemyUnits = function(enemyID) { if (enemyID === undefined) return this.getEnemyEntities().filter(m.Filters.byClass("Unit")); return this.updatingGlobalCollection("player-" + enemyID + "-units", m.Filters.byClass("Unit"), this.getEntities(enemyID)); }; /** if maintain is true, this will be stored. Otherwise it's one-shot. */ m.GameState.prototype.getOwnEntitiesByMetadata = function(key, value, maintain) { if (maintain === true) return this.updatingCollection(key + "-" + value, m.Filters.byMetadata(this.player, key, value),this.getOwnEntities()); return this.getOwnEntities().filter(m.Filters.byMetadata(this.player, key, value)); }; m.GameState.prototype.getOwnEntitiesByRole = function(role, maintain) { return this.getOwnEntitiesByMetadata("role", role, maintain); }; m.GameState.prototype.getOwnEntitiesByType = function(type, maintain) { let filter = m.Filters.byType(type); if (maintain === true) return this.updatingCollection("type-" + type, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnEntitiesByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("class-" + cls, filter, this.getOwnEntities()); return this.getOwnEntities().filter(filter); }; m.GameState.prototype.getOwnFoundationsByClass = function(cls, maintain) { let filter = m.Filters.byClass(cls); if (maintain) return this.updatingCollection("foundations-class-" + cls, filter, this.getOwnFoundations()); return this.getOwnFoundations().filter(filter); }; m.GameState.prototype.getOwnTrainingFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-training-facilities", m.Filters.byTrainingQueue(), this.getOwnEntities()); }; m.GameState.prototype.getOwnResearchFacilities = function() { return this.updatingGlobalCollection("player-" + this.player + "-research-facilities", m.Filters.byResearchAvailable(this, this.playerData.civ), this.getOwnEntities()); }; m.GameState.prototype.countEntitiesByType = function(type, maintain) { return this.getOwnEntitiesByType(type, maintain).length; }; m.GameState.prototype.countEntitiesAndQueuedByType = function(type, maintain) { let template = this.getTemplate(type); if (!template) return 0; let count = this.countEntitiesByType(type, maintain); // Count building foundations if (template.hasClass("Structure") === true) count += this.countFoundationsByType(type, true); else if (template.resourceSupplyType() !== undefined) // animal resources count += this.countEntitiesByType("resource|" + type, true); else { // Count entities in building production queues // TODO: maybe this fails for corrals. this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.unitTemplate == type) count += item.count; }); } return count; }; m.GameState.prototype.countFoundationsByType = function(type, maintain) { let foundationType = "foundation|" + type; if (maintain === true) return this.updatingCollection("foundation-type-" + type, m.Filters.byType(foundationType), this.getOwnFoundations()).length; let count = 0; this.getOwnStructures().forEach(function(ent) { if (ent.templateName() == foundationType) ++count; }); return count; }; m.GameState.prototype.countOwnEntitiesByRole = function(role) { return this.getOwnEntitiesByRole(role, "true").length; }; m.GameState.prototype.countOwnEntitiesAndQueuedWithRole = function(role) { let count = this.countOwnEntitiesByRole(role); // Count entities in building production queues this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata.role && item.metadata.role == role) count += item.count; }); return count; }; m.GameState.prototype.countOwnQueuedEntitiesWithMetadata = function(data, value) { // Count entities in building production queues let count = 0; this.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) if (item.metadata && item.metadata[data] && item.metadata[data] == value) count += item.count; }); return count; }; m.GameState.prototype.getOwnFoundations = function() { return this.updatingGlobalCollection("player-" + this.player + "-foundations", m.Filters.isFoundation(), this.getOwnStructures()); }; m.GameState.prototype.getOwnDropsites = function(resource) { if (resource) return this.updatingCollection("ownDropsite-" + resource, m.Filters.isDropsite(resource), this.getOwnEntities()); return this.updatingCollection("ownDropsite-all", m.Filters.isDropsite(), this.getOwnEntities()); }; m.GameState.prototype.getAnyDropsites = function(resource) { if (resource) return this.updatingGlobalCollection("anyDropsite-" + resource, m.Filters.isDropsite(resource), this.getEntities()); return this.updatingGlobalCollection("anyDropsite-all", m.Filters.isDropsite(), this.getEntities()); }; m.GameState.prototype.getResourceSupplies = function(resource) { return this.updatingGlobalCollection("resource-" + resource, m.Filters.byResource(resource), this.getEntities()); }; m.GameState.prototype.getHuntableSupplies = function() { return this.updatingGlobalCollection("resource-hunt", m.Filters.isHuntable(), this.getEntities()); }; m.GameState.prototype.getFishableSupplies = function() { return this.updatingGlobalCollection("resource-fish", m.Filters.isFishable(), this.getEntities()); }; /** This returns only units from buildings. */ m.GameState.prototype.findTrainableUnits = function(classes, anticlasses) { let allTrainable = []; let civ = this.playerData.civ; this.getOwnTrainingFacilities().forEach(function(ent) { let trainable = ent.trainableEntities(civ); if (!trainable) return; for (let unit of trainable) if (allTrainable.indexOf(unit) === -1) allTrainable.push(unit); }); let ret = []; let limits = this.getEntityLimits(); let current = this.getEntityCounts(); for (let trainable of allTrainable) { if (this.isTemplateDisabled(trainable)) continue; let template = this.getTemplate(trainable); if (!template || !template.available(this)) continue; if (classes.some(c => !template.hasClass(c))) continue; if (anticlasses.some(c => template.hasClass(c))) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; ret.push( [trainable, template] ); } return ret; }; /** * Return all techs which can currently be researched * Does not factor cost. * If there are pairs, both techs are returned. */ m.GameState.prototype.findAvailableTech = function() { let allResearchable = []; let civ = this.playerData.civ; for (let ent of this.getOwnEntities().values()) { let searchable = ent.researchableTechs(this, civ); if (!searchable) continue; for (let tech of searchable) if (!this.playerData.disabledTechnologies[tech] && allResearchable.indexOf(tech) === -1) allResearchable.push(tech); } let ret = []; for (let tech of allResearchable) { let template = this.getTemplate(tech); if (template.pairDef()) { let techs = template.getPairedTechs(); if (this.canResearch(techs[0]._templateName)) ret.push([techs[0]._templateName, techs[0]] ); if (this.canResearch(techs[1]._templateName)) ret.push([techs[1]._templateName, techs[1]] ); } else if (this.canResearch(tech)) { // Phases are treated separately if (this.phases.every(phase => template._templateName != phase.name)) ret.push( [tech, template] ); } } return ret; }; /** * Return true if we have a building able to train that template */ m.GameState.prototype.hasTrainer = function(template) { let civ = this.playerData.civ; for (let ent of this.getOwnTrainingFacilities().values()) { let trainable = ent.trainableEntities(civ); if (trainable && trainable.indexOf(template) !== -1) return true; } return false; }; /** * Find buildings able to train that template. */ m.GameState.prototype.findTrainers = function(template) { let civ = this.playerData.civ; return this.getOwnTrainingFacilities().filter(function(ent) { let trainable = ent.trainableEntities(civ); return trainable && trainable.indexOf(template) !== -1; }); }; /** * Get any unit that is capable of constructing the given building type. */ m.GameState.prototype.findBuilder = function(template) { let civ = this.getPlayerCiv(); for (let ent of this.getOwnUnits().values()) { let buildable = ent.buildableEntities(civ); if (buildable && buildable.indexOf(template) !== -1) return ent; } return undefined; }; /** Return true if one of our buildings is capable of researching the given tech */ m.GameState.prototype.hasResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return false; let civ = this.playerData.civ; for (let ent of this.getOwnResearchFacilities().values()) { let techs = ent.researchableTechs(this, civ); for (let tech of techs) { let temp = this.getTemplate(tech); if (temp.pairDef()) { let pairedTechs = temp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } } return false; }; /** Find buildings that are capable of researching the given tech */ m.GameState.prototype.findResearchers = function(templateName, noRequirementCheck) { // let's check we can research the tech. if (!this.canResearch(templateName, noRequirementCheck)) return undefined; let self = this; let civ = this.playerData.civ; return this.getOwnResearchFacilities().filter(function(ent) { let techs = ent.researchableTechs(self, civ); for (let tech of techs) { let thisTemp = self.getTemplate(tech); if (thisTemp.pairDef()) { let pairedTechs = thisTemp.getPairedTechs(); if (pairedTechs[0]._templateName == templateName || pairedTechs[1]._templateName == templateName) return true; } else if (tech == templateName) return true; } return false; }); }; m.GameState.prototype.getEntityLimits = function() { return this.playerData.entityLimits; }; m.GameState.prototype.getEntityCounts = function() { return this.playerData.entityCounts; }; m.GameState.prototype.isTemplateAvailable = function(templateName) { if (this.templates[templateName] === undefined) this.sharedScript.GetTemplate(templateName); return this.templates[templateName] && !this.isTemplateDisabled(templateName); }; m.GameState.prototype.isTemplateDisabled = function(templateName) { if (!this.playerData.disabledTemplates[templateName]) return false; return this.playerData.disabledTemplates[templateName]; }; /** Checks whether the maximum number of buildings have been constructed for a certain catergory */ m.GameState.prototype.isEntityLimitReached = function(category) { if (this.playerData.entityLimits[category] === undefined || this.playerData.entityCounts[category] === undefined) return false; return this.playerData.entityCounts[category] >= this.playerData.entityLimits[category]; }; m.GameState.prototype.getTraderTemplatesGains = function() { let shipMechantTemplateName = this.applyCiv("units/{civ}_ship_merchant"); let supportTraderTemplateName = this.applyCiv("units/{civ}_support_trader"); let shipMerchantTemplate = !this.isTemplateDisabled(shipMechantTemplateName) && this.getTemplate(shipMechantTemplateName); let supportTraderTemplate = !this.isTemplateDisabled(supportTraderTemplateName) && this.getTemplate(supportTraderTemplateName); let norm = TradeGainNormalization(this.sharedScript.mapSize); let ret = {}; if (supportTraderTemplate) ret.landGainMultiplier = norm * supportTraderTemplate.gainMultiplier(); if (shipMerchantTemplate) ret.navalGainMultiplier = norm * shipMerchantTemplate.gainMultiplier(); return ret; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/shared.js (revision 20737) @@ -1,395 +1,392 @@ var API3 = function(m) { /** Shared script handling templates and basic terrain analysis */ m.SharedScript = function(settings) { if (!settings) return; this._players = Object.keys(settings.players).map(key => settings.players[key]); // TODO SM55 Object.values(settings.players) this._templates = settings.templates; - this._techTemplates = settings.techTemplates; this._entityMetadata = {}; for (let player of this._players) this._entityMetadata[player] = {}; // array of entity collections this._entityCollections = new Map(); this._entitiesModifications = new Map(); // entities modifications this._templatesModifications = {}; // template modifications // each name is a reference to the actual one. this._entityCollectionsName = new Map(); this._entityCollectionsByDynProp = {}; this._entityCollectionsUID = 0; }; /** Return a simple object (using no classes etc) that will be serialized into saved games */ m.SharedScript.prototype.Serialize = function() { return { "players": this._players, - "techTemplates": this._techTemplates, "templatesModifications": this._templatesModifications, "entitiesModifications": this._entitiesModifications, "metadata": this._entityMetadata }; }; /** * Called after the constructor when loading a saved game, with 'data' being * whatever Serialize() returned */ m.SharedScript.prototype.Deserialize = function(data) { this._players = data.players; - this._techTemplates = data.techTemplates; this._templatesModifications = data.templatesModifications; this._entitiesModifications = data.entitiesModifications; this._entityMetadata = data.metadata; this.isDeserialized = true; }; m.SharedScript.prototype.GetTemplate = function(name) { if (this._templates[name] === undefined) this._templates[name] = Engine.GetTemplate(name) || null; return this._templates[name]; }; /** * Initialize the shared component. * We need to know the initial state of the game for this, as we will use it. * This is called right at the end of the map generation. */ m.SharedScript.prototype.init = function(state, deserialization) { if (!deserialization) this._entitiesModifications = new Map(); this.ApplyTemplatesDelta(state); this.passabilityClasses = state.passabilityClasses; this.playersData = state.players; this.timeElapsed = state.timeElapsed; this.circularMap = state.circularMap; this.mapSize = state.mapSize; this.gameType = state.gameType; this.alliedVictory = state.alliedVictory; this.ceasefireActive = state.ceasefireActive; this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; this.passabilityMap = state.passabilityMap; if (this.mapSize % this.passabilityMap.width !== 0) error("AI shared component inconsistent sizes: map=" + this.mapSize + " while passability=" + this.passabilityMap.width); this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; this.territoryMap = state.territoryMap; if (this.mapSize % this.territoryMap.width !== 0) error("AI shared component inconsistent sizes: map=" + this.mapSize + " while territory=" + this.territoryMap.width); this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; /* let landPassMap = new Uint8Array(this.passabilityMap.data.length); let waterPassMap = new Uint8Array(this.passabilityMap.data.length); let obstructionMaskLand = this.passabilityClasses["default-terrain-only"]; let obstructionMaskWater = this.passabilityClasses["ship-terrain-only"]; for (let i = 0; i < this.passabilityMap.data.length; ++i) { landPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskLand) ? 0 : 255; waterPassMap[i] = (this.passabilityMap.data[i] & obstructionMaskWater) ? 0 : 255; } Engine.DumpImage("LandPassMap.png", landPassMap, this.passabilityMap.width, this.passabilityMap.height, 255); Engine.DumpImage("WaterPassMap.png", waterPassMap, this.passabilityMap.width, this.passabilityMap.height, 255); */ this._entities = new Map(); if (state.entities) for (let id in state.entities) this._entities.set(+id, new m.Entity(this, state.entities[id])); // entity collection updated on create/destroy event. this.entities = new m.EntityCollection(this, this._entities); // create the terrain analyzer this.terrainAnalyzer = new m.TerrainAnalysis(); this.terrainAnalyzer.init(this, state); this.accessibility = new m.Accessibility(); this.accessibility.init(state, this.terrainAnalyzer); // Resource types: ignore = not used for resource maps // abundant = abundant resource with small amount each // sparse = sparse resource, but huge amount each // The following maps are defined in TerrainAnalysis.js and are used for some building placement (cc, dropsites) // They are updated by checking for create and destroy events for all resources this.normalizationFactor = { "abundant": 50, "sparse": 90 }; this.influenceRadius = { "abundant": 36, "sparse": 48 }; this.ccInfluenceRadius = { "abundant": 60, "sparse": 120 }; this.resourceMaps = {}; // Contains maps showing the density of resources this.ccResourceMaps = {}; // Contains maps showing the density of resources, optimized for CC placement. this.createResourceMaps(); this.gameState = {}; for (let player of this._players) { this.gameState[player] = new m.GameState(); this.gameState[player].init(this,state, player); } }; /** * General update of the shared script, before each AI's update * applies entity deltas, and each gamestate. */ m.SharedScript.prototype.onUpdate = function(state) { if (this.isDeserialized) { this.init(state, true); this.isDeserialized = false; } // deals with updating based on create and destroy messages. this.ApplyEntitiesDelta(state); this.ApplyTemplatesDelta(state); Engine.ProfileStart("onUpdate"); // those are dynamic and need to be reset as the "state" object moves in memory. this.events = state.events; this.passabilityClasses = state.passabilityClasses; this.playersData = state.players; this.timeElapsed = state.timeElapsed; this.barterPrices = state.barterPrices; this.ceasefireActive = state.ceasefireActive; this.ceasefireTimeRemaining = state.ceasefireTimeRemaining / 1000; this.passabilityMap = state.passabilityMap; this.passabilityMap.cellSize = this.mapSize / this.passabilityMap.width; this.territoryMap = state.territoryMap; this.territoryMap.cellSize = this.mapSize / this.territoryMap.width; for (let i in this.gameState) this.gameState[i].update(this); // TODO: merge this with "ApplyEntitiesDelta" since after all they do the same. this.updateResourceMaps(this.events); Engine.ProfileStop(); }; m.SharedScript.prototype.ApplyEntitiesDelta = function(state) { Engine.ProfileStart("Shared ApplyEntitiesDelta"); let foundationFinished = {}; // by order of updating: // we "Destroy" last because we want to be able to switch Metadata first. for (let evt of state.events.Create) { if (!state.entities[evt.entity]) continue; // Sometimes there are things like foundations which get destroyed too fast let entity = new m.Entity(this, state.entities[evt.entity]); this._entities.set(evt.entity, entity); this.entities.addEnt(entity); // Update all the entity collections since the create operation affects static properties as well as dynamic for (let entCol of this._entityCollections.values()) entCol.updateEnt(entity); } for (let evt of state.events.EntityRenamed) { // Switch the metadata: TODO entityCollections are updated only because of the owner change. Should be done properly for (let player of this._players) { this._entityMetadata[player][evt.newentity] = this._entityMetadata[player][evt.entity]; this._entityMetadata[player][evt.entity] = {}; } } for (let evt of state.events.TrainingFinished) { // Apply metadata stored in training queues for (let entId of evt.entities) if (this._entities.has(entId)) for (let key in evt.metadata) this.setMetadata(evt.owner, this._entities.get(entId), key, evt.metadata[key]); } for (let evt of state.events.ConstructionFinished) { // metada are already moved by EntityRenamed when needed (i.e. construction, not repair) if (evt.entity != evt.newentity) foundationFinished[evt.entity] = true; } for (let evt of state.events.AIMetadata) { if (!this._entities.has(evt.id)) continue; // might happen in some rare cases of foundations getting destroyed, perhaps. // Apply metadata (here for buildings for example) for (let key in evt.metadata) this.setMetadata(evt.owner, this._entities.get(evt.id), key, evt.metadata[key]); } for (let evt of state.events.Destroy) { if (!this._entities.has(evt.entity)) continue;// probably should remove the event. if (foundationFinished[evt.entity]) evt.SuccessfulFoundation = true; // The entity was destroyed but its data may still be useful, so // remember the entity and this AI's metadata concerning it evt.metadata = {}; evt.entityObj = this._entities.get(evt.entity); for (let player of this._players) evt.metadata[player] = this._entityMetadata[player][evt.entity]; let entity = this._entities.get(evt.entity); for (let entCol of this._entityCollections.values()) entCol.removeEnt(entity); this.entities.removeEnt(entity); this._entities.delete(evt.entity); this._entitiesModifications.delete(evt.entity); for (let player of this._players) delete this._entityMetadata[player][evt.entity]; } for (let id in state.entities) { let changes = state.entities[id]; let entity = this._entities.get(+id); for (let prop in changes) { entity._entity[prop] = changes[prop]; this.updateEntityCollections(prop, entity); } } // apply per-entity aura-related changes. // this supersedes tech-related changes. for (let id in state.changedEntityTemplateInfo) { if (!this._entities.has(+id)) continue; // dead, presumably. let changes = state.changedEntityTemplateInfo[id]; if (!this._entitiesModifications.has(+id)) this._entitiesModifications.set(+id, new Map()); let modif = this._entitiesModifications.get(+id); for (let change of changes) modif.set(change.variable, change.value); } Engine.ProfileStop(); }; m.SharedScript.prototype.ApplyTemplatesDelta = function(state) { Engine.ProfileStart("Shared ApplyTemplatesDelta"); for (let player in state.changedTemplateInfo) { let playerDiff = state.changedTemplateInfo[player]; for (let template in playerDiff) { let changes = playerDiff[template]; if (!this._templatesModifications[template]) this._templatesModifications[template] = {}; if (!this._templatesModifications[template][player]) this._templatesModifications[template][player] = new Map(); let modif = this._templatesModifications[template][player]; for (let change of changes) modif.set(change.variable, change.value); } } Engine.ProfileStop(); }; m.SharedScript.prototype.registerUpdatingEntityCollection = function(entCollection) { entCollection.setUID(this._entityCollectionsUID); this._entityCollections.set(this._entityCollectionsUID, entCollection); for (let prop of entCollection.dynamicProperties()) { if (!this._entityCollectionsByDynProp[prop]) this._entityCollectionsByDynProp[prop] = new Map(); this._entityCollectionsByDynProp[prop].set(this._entityCollectionsUID, entCollection); } this._entityCollectionsUID++; }; m.SharedScript.prototype.removeUpdatingEntityCollection = function(entCollection) { let uid = entCollection.getUID(); if (this._entityCollections.has(uid)) this._entityCollections.delete(uid); for (let prop of entCollection.dynamicProperties()) if (this._entityCollectionsByDynProp[prop].has(uid)) this._entityCollectionsByDynProp[prop].delete(uid); }; m.SharedScript.prototype.updateEntityCollections = function(property, ent) { if (this._entityCollectionsByDynProp[property] === undefined) return; for (let entCol of this._entityCollectionsByDynProp[property].values()) entCol.updateEnt(ent); }; m.SharedScript.prototype.setMetadata = function(player, ent, key, value) { let metadata = this._entityMetadata[player][ent.id()]; if (!metadata) metadata = this._entityMetadata[player][ent.id()] = {}; metadata[key] = value; this.updateEntityCollections('metadata', ent); this.updateEntityCollections('metadata.' + key, ent); }; m.SharedScript.prototype.getMetadata = function(player, ent, key) { let metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return undefined; return metadata[key]; }; m.SharedScript.prototype.deleteMetadata = function(player, ent, key) { let metadata = this._entityMetadata[player][ent.id()]; if (!metadata || !(key in metadata)) return true; metadata[key] = undefined; delete metadata[key]; this.updateEntityCollections('metadata', ent); this.updateEntityCollections('metadata.' + key, ent); return true; }; m.copyPrototype = function(descendant, parent) { let sConstructor = parent.toString(); let aMatch = sConstructor.match( /\s*function (.*)\(/ ); if ( aMatch != null ) descendant.prototype[aMatch[1]] = parent; for (let p in parent.prototype) descendant.prototype[p] = parent.prototype[p]; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/technology.js (revision 20737) @@ -1,157 +1,157 @@ +LoadModificationTemplates(); + var API3 = function(m) { /** Wrapper around a technology template */ -m.Technology = function(allTemplates, templateName) +m.Technology = function(templateName) { this._templateName = templateName; - let template = allTemplates[templateName]; + let template = TechnologyTemplates.Get(templateName); // check if this is one of two paired technologies. this._isPair = template.pair !== undefined; if (this._isPair) { - if (allTemplates[template.pair].top == templateName) - this._pairedWith = allTemplates[template.pair].bottom; - else - this._pairedWith = allTemplates[template.pair].top; + let pairTech = TechnologyTemplates.Get(template.pair); + this._pairedWith = pairTech.top == templateName ? pairTech.bottom : pairTech.top; } + // check if it only defines a pair: this._definesPair = template.top !== undefined; this._template = template; - this._techTemplates = allTemplates; }; /** returns generic, or specific if civ provided. */ m.Technology.prototype.name = function(civ) { if (civ === undefined) return this._template.genericName; if (this._template.specificName === undefined || this._template.specificName[civ] === undefined) return undefined; return this._template.specificName[civ]; }; m.Technology.prototype.pairDef = function() { return this._definesPair; }; /** in case this defines a pair only, returns the two paired technologies. */ m.Technology.prototype.getPairedTechs = function() { if (!this._definesPair) return undefined; - let techOne = new m.Technology(this._techTemplates, this._template.top); - let techTwo = new m.Technology(this._techTemplates, this._template.bottom); - - return [techOne,techTwo]; + return [ + new m.Technology(this._template.top), + new m.Technology(this._template.bottom) + ]; }; m.Technology.prototype.pair = function() { if (!this._isPair) return undefined; return this._template.pair; }; m.Technology.prototype.pairedWith = function() { if (!this._isPair) return undefined; return this._pairedWith; }; m.Technology.prototype.cost = function(productionQueue) { if (!this._template.cost) return undefined; let cost = {}; for (let type in this._template.cost) { cost[type] = +this._template.cost[type]; if (productionQueue) cost[type] *= productionQueue.techCostMultiplier(type); } return cost; }; m.Technology.prototype.costSum = function(productionQueue) { let cost = this.cost(productionQueue); if (!cost) return undefined; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }; m.Technology.prototype.researchTime = function() { if (!this._template.researchTime) return undefined; return this._template.researchTime; }; m.Technology.prototype.requirements = function(civ) { return DeriveTechnologyRequirements(this._template, civ); }; m.Technology.prototype.autoResearch = function() { if (!this._template.autoResearch) return undefined; return this._template.autoResearch; }; m.Technology.prototype.supersedes = function() { if (!this._template.supersedes) return undefined; return this._template.supersedes; }; m.Technology.prototype.modifications = function() { if (!this._template.modifications) return undefined; return this._template.modifications; }; m.Technology.prototype.affects = function() { if (!this._template.affects) return undefined; return this._template.affects; }; m.Technology.prototype.isAffected = function(classes) { if (!this._template.affects) return false; for (let affect of this._template.affects) { let reqClasses = affect.split(" "); let fitting = true; for (let reqClass of reqClasses) { if (classes.indexOf(reqClass) !== -1) continue; fitting = false; break; } if (fitting === true) return true; } return false; }; return m; }(API3); Index: ps/trunk/binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/Auras.js (revision 20737) @@ -1,487 +1,488 @@ function Auras() {} Auras.prototype.Schema = "" + "tokens" + "" + ""; Auras.prototype.Init = function() { - let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); - this.auras = {}; this.affectedPlayers = {}; + for (let name of this.GetAuraNames()) - { this.affectedPlayers[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) + if (AuraTemplates.Get(name).stackable) return name + this.entity; return name; }; Auras.prototype.GetDescriptions = function() { var ret = {}; for (let auraID of this.GetAuraNames()) { - let aura = this.auras[auraID]; + let aura = AuraTemplates.Get(auraID); ret[auraID] = { "name": aura.auraName, "description": aura.auraDescription || null, "radius": this.GetRange(auraID) || null }; } return ret; }; Auras.prototype.GetAuraNames = function() { return this.template._string.split(/\s+/); }; Auras.prototype.GetOverlayIcon = function(name) { - return this.auras[name].overlayIcon || ""; + return AuraTemplates.Get(name).overlayIcon || ""; }; Auras.prototype.GetAffectedEntities = function(name) { return this[name].targetUnits; }; Auras.prototype.GetRange = function(name) { if (this.IsRangeAura(name)) - return +this.auras[name].radius; + return +AuraTemplates.Get(name).radius; return undefined; }; Auras.prototype.GetClasses = function(name) { - return this.auras[name].affects; + return AuraTemplates.Get(name).affects; }; Auras.prototype.GetModifications = function(name) { - return this.auras[name].modifications; + return AuraTemplates.Get(name).modifications; }; Auras.prototype.GetAffectedPlayers = function(name) { return this.affectedPlayers[name]; }; Auras.prototype.GetRangeOverlays = function() { let rangeOverlays = []; for (let name of this.GetAuraNames()) { if (!this.IsRangeAura(name) || !this[name].isApplied) continue; + let rangeOverlay = AuraTemplates.Get(name).rangeOverlay; + rangeOverlays.push( - this.auras[name].rangeOverlay ? + rangeOverlay ? { "radius": this.GetRange(name), - "texture": this.auras[name].rangeOverlay.lineTexture, - "textureMask": this.auras[name].rangeOverlay.lineTextureMask, - "thickness": this.auras[name].rangeOverlay.lineThickness + "texture": rangeOverlay.lineTexture, + "textureMask": rangeOverlay.lineTextureMask, + "thickness": rangeOverlay.lineThickness } : // Specify default in order not to specify it in about 40 auras { "radius": this.GetRange(name), "texture": "outline_border.png", "textureMask": "outline_border_mask.png", "thickness": 0.2 }); } return rangeOverlays; }; Auras.prototype.CalculateAffectedPlayers = function(name) { - var affectedPlayers = this.auras[name].affectedPlayers || ["Player"]; + var affectedPlayers = AuraTemplates.Get(name).affectedPlayers || ["Player"]; this.affectedPlayers[name] = []; var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!cmpPlayer) cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer || cmpPlayer.GetState() == "defeated") 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) + if (!AuraTemplates.Get(name).requiredTechnology) return true; + let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (!cmpTechnologyManager) return false; - return cmpTechnologyManager.IsTechnologyResearched(this.auras[name].requiredTechnology); + + return cmpTechnologyManager.IsTechnologyResearched(AuraTemplates.Get(name).requiredTechnology); }; 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; + return AuraTemplates.Get(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) { return this.GetType(name) == "range"; }; Auras.prototype.IsGlobalAura = function(name) { return this.GetType(name) == "global" || this.GetType(name) == "player"; }; Auras.prototype.IsPlayerAura = function(name) { return this.GetType(name) == "player"; }; /** * 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 = {}; let needVisualizationUpdate = false; // remove all bonuses for (let name of auraNames) { targetUnitsClone[name] = []; if (!this[name]) continue; if (this.IsRangeAura(name)) needVisualizationUpdate = true; 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.IsGlobalAura(name)) { for (let player of affectedPlayers) { this.ApplyTemplateBonus(name, affectedPlayers); if (this.IsPlayerAura(name)) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnts = affectedPlayers.map(player => cmpPlayerManager.GetPlayerByID(player)); this.ApplyBonus(name, playerEnts); } else this.ApplyBonus(name, cmpRangeManager.GetEntitiesByPlayer(player)); } continue; } if (!this.IsRangeAura(name)) { this.ApplyBonus(name, targetUnitsClone[name]); continue; } needVisualizationUpdate = true; if (this[name].isApplied) { this[name].rangeQuery = cmpRangeManager.CreateActiveQuery( this.entity, 0, this.GetRange(name), affectedPlayers, IID_Identity, cmpRangeManager.GetEntityFlagMask("normal") ); cmpRangeManager.EnableActiveQuery(this[name].rangeQuery); } } if (needVisualizationUpdate) { let cmpRangeOverlayManager = Engine.QueryInterface(this.entity, IID_RangeOverlayManager); if (cmpRangeOverlayManager) { cmpRangeOverlayManager.UpdateRangeOverlays("Auras"); cmpRangeOverlayManager.RegenerateRangeOverlayManagers(false); } } }; 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) { for (let name of this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery)) { this.ApplyBonus(name, msg.added); this.RemoveBonus(name, msg.removed); } }; Auras.prototype.OnGarrisonedUnitsChanged = function(msg) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n))) { this.ApplyBonus(name, msg.added); this.RemoveBonus(name, msg.removed); } }; Auras.prototype.RegisterGlobalOwnershipChanged = function(msg) { for (let name of this.GetAuraNames().filter(n => this.IsGlobalAura(n))) { let affectedPlayers = this.GetAffectedPlayers(name); let wasApplied = affectedPlayers.indexOf(msg.from) != -1; let willBeApplied = affectedPlayers.indexOf(msg.to) != -1; if (wasApplied && !willBeApplied) this.RemoveBonus(name, [msg.entity]); if (willBeApplied && !wasApplied) this.ApplyBonus(name, [msg.entity]); } }; Auras.prototype.ApplyFormationBonus = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.ApplyBonus(name, memberList); }; Auras.prototype.ApplyGarrisonBonus = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) 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); cmpAuraManager.RegisterGlobalAuraSource(this.entity); 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) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) this.RemoveBonus(name, memberList); }; Auras.prototype.RemoveGarrisonBonus = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) this.RemoveBonus(name, [structure]); }; Auras.prototype.RemoveTemplateBonus = function(name) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); cmpAuraManager.UnregisterGlobalAuraSource(this.entity); var modifications = this.GetModifications(name); 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 cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && (cmpPlayer.GetPlayerID() == msg.player || cmpPlayer.GetPlayerID() == msg.otherPlayer) || IsOwnedByPlayer(msg.player, this.entity) || IsOwnedByPlayer(msg.otherPlayer, this.entity)) this.Clean(); }; Auras.prototype.OnGlobalResearchFinished = function(msg) { var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if ((!cmpPlayer || cmpPlayer.GetPlayerID() != msg.player) && !IsOwnedByPlayer(msg.player, this.entity)) return; for (let name of this.GetAuraNames()) { - let requiredTech = this.auras[name].requiredTechnology; + let requiredTech = AuraTemplates.Get(name).requiredTechnology; if (requiredTech && requiredTech == msg.tech) { this.Clean(); return; } } }; Auras.prototype.OnPlayerDefeated = function(msg) { this.Clean(); }; Engine.RegisterComponentType(IID_Auras, "Auras", Auras); Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 20737) @@ -1,2035 +1,2026 @@ 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(); this.enabledVisualRangeOverlayTypes = {}; }; /* * 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() { let ret = { "players": [] }; let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits); // Work out what phase we are in let phase = ""; let cmpTechnologyManager = QueryPlayerIDInterface(i, 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(), "panelEntities": cmpPlayer.GetPanelEntities(), "resourceCounts": cmpPlayer.GetResourceCounts(), "trainingBlocked": cmpPlayer.IsTrainingBlocked(), "state": cmpPlayer.GetState(), "team": cmpPlayer.GetTeam(), "teamsLocked": cmpPlayer.GetLockTeams(), "cheatsEnabled": cmpPlayer.GetCheatsEnabled(), "disabledTemplates": cmpPlayer.GetDisabledTemplates(), "disabledTechnologies": cmpPlayer.GetDisabledTechnologies(), "hasSharedDropsites": cmpPlayer.HasSharedDropsites(), "hasSharedLos": cmpPlayer.HasSharedLos(), "spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(), "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.GetStartedTechs() : null, "researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null, "classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null, "typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null, "canBarter": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).PlayerHasMarket(i), "barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(i) }); } 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 = cmpTerrain.GetMapSize(); // 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 cinema path info let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); if (cmpCinemaManager) ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); // Add the game type and allied victory let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); ret.gameType = cmpEndGameManager.GetGameType(); ret.alliedVictory = cmpEndGameManager.GetAlliedVictory(); // Add basic statistics to each player for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, 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() { // Get basic simulation info let ret = this.GetSimulationState(); // Add statistics to each player let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker); if (cmpPlayerStatisticsTracker) ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences(); } 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() { 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, "canGarrison": null, "identity": null, "fogging": null, "foundation": null, "garrisonHolder": null, "gate": null, "guard": null, "market": null, "mirage": null, "pack": null, "upgrade" : 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(), "canDelete": !cmpIdentity.IsUndeletable() }; 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(); } 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 = QueryMiragedInterface(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(), }; var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade); if (cmpUpgrade) ret.upgrade = { "upgrades" : cmpUpgrade.GetUpgrades(), "progress": cmpUpgrade.GetProgress(), "template": cmpUpgrade.GetUpgradingTo() }; let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue); if (cmpProductionQueue) ret.production = { "entities": cmpProductionQueue.GetEntitiesList(), "technologies": cmpProductionQueue.GetTechnologiesList(), "techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(), "queue": cmpProductionQueue.GetQueue() }; let cmpTrader = Engine.QueryInterface(ent, IID_Trader); if (cmpTrader) ret.trader = { "goods": cmpTrader.GetGoods() }; let cmpFogging = Engine.QueryInterface(ent, IID_Fogging); if (cmpFogging) ret.fogging = { "mirage": cmpFogging.IsMiraged(player) ? cmpFogging.GetMirage(player) : 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(), "buffHeal": cmpGarrisonHolder.GetHealRate(), "allowedClasses": cmpGarrisonHolder.GetAllowedClasses(), "capacity": cmpGarrisonHolder.GetCapacity(), "garrisonedEntitiesCount": cmpGarrisonHolder.GetGarrisonedEntitiesCount() }; ret.canGarrison = !!Engine.QueryInterface(ent, IID_Garrisonable); 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(), "canPatrol": cmpUnitAI.CanPatrol(), "possibleStances": cmpUnitAI.GetPossibleStances(), "isIdle":cmpUnitAI.IsIdle(), }; 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, "buildingAI": null, "deathDamage": null, "heal": null, "isBarterMarket": null, "loot": null, "obstruction": null, "turretParent":null, "promotion": null, "repairRate": null, "buildRate": null, "buildTime": null, "resourceDropsite": null, "resourceGatherRates": null, "resourceSupply": null, "resourceTrickle": null, "speed": 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); ret.attack[type].splash = cmpAttack.GetSplashDamage(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 range 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(), "maxArrowCount": cmpBuildingAI.GetMaxArrowCount(), "garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(), "garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(), "arrowCount": cmpBuildingAI.GetArrowCount() }; let cmpDeathDamage = Engine.QueryInterface(ent, IID_DeathDamage); if (cmpDeathDamage) ret.deathDamage = cmpDeathDamage.GetDeathDamageStrengths(); 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(); ret.buildTime = cmpFoundation.GetBuildTime(); } 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")) ret.isBarterMarket = true; let cmpHeal = Engine.QueryInterface(ent, IID_Heal); if (cmpHeal) ret.heal = { "hp": cmpHeal.GetHP(), "range": cmpHeal.GetRange().max, "rate": cmpHeal.GetRate(), "unhealableClasses": cmpHeal.GetUnhealableClasses(), "healableClasses": cmpHeal.GetHealableClasses(), }; let cmpLoot = Engine.QueryInterface(ent, IID_Loot); if (cmpLoot) { let resources = cmpLoot.GetResources(); ret.loot = { "xp": cmpLoot.GetXp() }; for (let res of Resources.GetCodes()) ret.loot[res] = resources[res]; } let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle); if (cmpResourceTrickle) { ret.resourceTrickle = { "interval": cmpResourceTrickle.GetTimer(), "rates": {} }; let rates = cmpResourceTrickle.GetRates(); for (let res in rates) ret.resourceTrickle.rates[res] = rates[res]; } let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); if (cmpUnitMotion) ret.speed = { "walk": cmpUnitMotion.GetWalkSpeed(), "run": cmpUnitMotion.GetRunSpeed() }; 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, name) { 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, Resources, DamageTypes); - // Add aura name and description loaded from JSON file let auraNames = template.Auras._string.split(/\s+/); - let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); + for (let name of auraNames) - aurasTemplate[name] = cmpDataTemplateManager.GetAuraTemplate(name); + aurasTemplate[name] = AuraTemplates.Get(name); + return GetTemplateDataHelper(template, player, aurasTemplate, Resources, DamageTypes); }; GuiInterface.prototype.GetTechnologyData = function(player, data) { - let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); - let template = cmpDataTemplateManager.GetTechnologyTemplate(data.name); - - if (!template) - { - warn("Tried to get data for invalid technology: " + data.name); - return null; - } - - return GetTechnologyDataHelper(template, data.civ, Resources); + return GetTechnologyDataHelper(TechnologyTemplates.Get(data.name), data.civ, Resources); }; 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) { let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager); if (!cmpTechnologyManager) return {}; let ret = {}; for (let tech of cmpTechnologyManager.GetStartedTechs()) { 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) { let cmpBattleDetection = QueryPlayerIDInterface(player, 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) { notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers(); notification.players[0] = -1; } 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(player) { 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(player) != -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 (let ent of 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 (let ent of 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 (let ent of 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 playerColors = {}; // cache of owner -> color map for (let ent of 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 = QueryPlayerIDInterface(owner); if (cmpPlayer) color = cmpPlayer.GetColor(); playerColors[owner] = color; } cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (!cmpRangeOverlayManager || player != owner && player != -1) continue; cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false); } }; GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data) { this.enabledVisualRangeOverlayTypes[data.type] = data.enabled; }; GuiInterface.prototype.GetEntitiesWithStatusBars = function() { return Array.from(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.SetRangeOverlays = function(player, cmd) { for (let ent of cmd.entities) { let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true); } }; 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 (let ent of 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 (let ent of 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 (let posi of 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); } } }; GuiInterface.prototype.AddTargetMarker = function(player, cmd) { let ent = Engine.AddLocalEntity(cmd.template); if (!ent) return; let cmpPosition = Engine.QueryInterface(ent, IID_Position); cmpPosition.JumpTo(cmd.x, cmd.z); }; /** * 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(); let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager); if (cmpRangeOverlayManager) cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes); // 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 (let ent of 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 (let ent of 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 (let type in wallSet.templates) { if (type == "curves") continue; let tpl = wallSet.templates[type]; 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": { "population": 0, "populationBonus": 0, "time": 0 }, }; for (let res of Resources.GetCodes()) result.cost[res] = 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. for (let res of Resources.GetCodes().concat(["population", "populationBonus", "time"])) result.cost[res] += tplData.cost[res]; } 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 (let ent of 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.PlacementType == "shore") { 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); }; /** * Find any idle units. * * @param data.idleClasses Array of class names to include. * @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined. * @param data.limit The number of idle units to return. May be left undefined (will return all idle units). * @param data.excludeUnits Array of units to exclude. * * Returns an array of idle units. * If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class. */ GuiInterface.prototype.FindIdleUnits = function(player, data) { let idleUnits = []; // The general case is that only the 'first' idle unit is required; filtering would examine every unit. // This loop imitates a grouping/aggregation on the first matching idle class. let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let entity of cmpRangeManager.GetEntitiesByPlayer(player)) { let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits); if (!filtered.idle) continue; // If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any. // By adding to the 'end', there is no pause if the series of units loops. var bucket = filtered.bucket; if(bucket == 0 && data.prevUnit && entity <= data.prevUnit) bucket = data.idleClasses.length; if (!idleUnits[bucket]) idleUnits[bucket] = []; idleUnits[bucket].push(entity); // If enough units have been collected in the first bucket, go ahead and return them. if (data.limit && bucket == 0 && idleUnits[0].length == data.limit) return idleUnits[0]; } let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []); if (data.limit && reduced.length > data.limit) return reduced.slice(0, data.limit); return reduced; }; /** * Discover if the player has idle units. * * @param data.idleClasses Array of class names to include. * @param data.excludeUnits Array of units to exclude. * * Returns a boolean of whether the player has any idle units */ GuiInterface.prototype.HasIdleUnits = function(player, data) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle); }; /** * Whether to filter an idle unit * * @param unit The unit to filter. * @param idleclasses Array of class names to include. * @param excludeUnits Array of units to exclude. * * Returns an object with the following fields: * - idle - true if the unit is considered idle by the filter, false otherwise. * - bucket - if idle, set to the index of the first matching idle class, undefined otherwise. */ GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits) { let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI); if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned()) return { "idle": false }; let cmpIdentity = Engine.QueryInterface(unit, IID_Identity); if(!cmpIdentity) return { "idle": false }; let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem)); if (bucket == -1 || excludeUnits.indexOf(unit) > -1) return { "idle": false }; return { "idle": true, "bucket": bucket }; }; 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.CanAttack = function(player, data) { let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack); return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined); }; /* * 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 (let ent of 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) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader)); let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 }; let shipTrader = { "total": 0, "trading": 0 }; for (let ent of 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) { return QueryPlayerIDInterface(player).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, "AddTargetMarker": 1, "SetBuildingPlacementPreview": 1, "SetWallPlacementPreview": 1, "GetFoundationSnapData": 1, "PlaySound": 1, "FindIdleUnits": 1, "HasIdleUnits": 1, "GetTradingRouteGain": 1, "GetTradingDetails": 1, "CanAttack": 1, "GetBatchTime": 1, "IsMapRevealed": 1, "SetPathfinderDebugOverlay": 1, "SetPathfinderHierDebugOverlay": 1, "SetObstructionDebugOverlay": 1, "SetMotionDebugOverlay": 1, "SetRangeDebugOverlay": 1, "EnableVisualRangeOverlayType": 1, "SetRangeOverlays": 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 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js (revision 20737) @@ -1,876 +1,873 @@ 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/{native}_support_trader\n units/athen_infantry_spearman_b\n " + "" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + "" + "" + "tokens" + "" + "" + "" + "" + "" + Resources.BuildSchema("nonNegativeDecimal", ["time"]) + ""; 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; let string = this.template.Entities._string; if (!string) return; // Replace the "{civ}" and "{native}" codes with the owner's civ ID and entity's civ ID. let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let cmpPlayer = QueryOwnerInterface(this.entity); if (!cmpPlayer) return; let cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity); if (cmpIdentity) string = string.replace(/\{native\}/g, cmpIdentity.GetCiv()); let entitiesList = string.replace(/\{civ\}/g, cmpPlayer.GetCiv()).split(/\s+/); // filter out disabled and invalid entities let disabledEntities = cmpPlayer.GetDisabledTemplates(); entitiesList = entitiesList.filter(ent => !disabledEntities[ent] && cmpTemplateManager.TemplateExists(ent)); // check if some templates need to show their advanced or elite version let upgradeTemplate = function(templateName) { let template = cmpTemplateManager.GetTemplate(templateName); while (template && template.Promotion !== undefined) { let 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+/); // Replace the civ specific technologies - let cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); for (let i = 0; i < techs.length; ++i) { let tech = techs[i]; if (tech.indexOf("{civ}") == -1) continue; let civTech = tech.replace("{civ}", cmpPlayer.GetCiv()); - techs[i] = cmpDataTemplateManager.TechnologyExists(civTech) ? - civTech : tech.replace("{civ}", "generic"); + techs[i] = TechnologyTemplates.Has(civTech) ? civTech : tech.replace("{civ}", "generic"); } // Remove any technologies that can't be researched by this civ - techs = techs.filter(tech => { - let reqs = DeriveTechnologyRequirements(cmpTechnologyManager.GetTechnologyTemplate(tech), cmpPlayer.GetCiv()); - return cmpTechnologyManager.CheckTechnologyRequirements(reqs, true); - }); + techs = techs.filter(tech => + cmpTechnologyManager.CheckTechnologyRequirements( + DeriveTechnologyRequirements(TechnologyTemplates.Get(tech), cmpPlayer.GetCiv()), + 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); + + let template = TechnologyTemplates.Get(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); + let template = TechnologyTemplates.Get(tech); if (template.top) ret[i] = {"pair": true, "top": template.top, "bottom": template.bottom}; else ret[i] = tech; } return ret; }; ProductionQueue.prototype.GetTechCostMultiplier = function() { let techCostMultiplier = {}; for (let res in this.template.TechCostMultiplier) techCostMultiplier[res] = ApplyValueModificationsToEntity("ProductionQueue/TechCostMultiplier/"+res, +this.template.TechCostMultiplier[res], this.entity); return techCostMultiplier; }; ProductionQueue.prototype.IsTechnologyResearchedOrInProgress = function(tech) { if (!tech) return false; var cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); - var template = cmpTechnologyManager.GetTechnologyTemplate(tech); + let template = TechnologyTemplates.Get(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) let cmpPlayer = QueryOwnerInterface(this.entity); if (this.queue.length < MAX_QUEUE_SIZE) { if (type == "unit") { if (!Number.isInteger(count) || count <= 0) { error("Invalid batch count " + count); return; } // 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 (let res in template.Cost.Resources) { costs[res] = ApplyValueModificationsToTemplate("Cost/Resources/"+res, +template.Cost.Resources[res], cmpPlayer.GetPlayerID(), template); totalCosts[res] = Math.floor(count * costs[res]); } 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 cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); - var template = cmpDataTemplateManager.GetTechnologyTemplate(templateName); - if (!template) + if (!TechnologyTemplates.Has(templateName)) return; if (!this.GetTechnologiesList().some(tech => tech && (tech == templateName || tech.pair && (tech.top == templateName || tech.bottom == templateName)))) { error("This entity cannot research " + templateName); return; } + let template = TechnologyTemplates.Get(templateName); let techCostMultiplier = this.GetTechCostMultiplier(); let time = techCostMultiplier.time * template.researchTime * cmpPlayer.GetCheatTimeMultiplier(); let cost = {}; for (let res in template.cost) cost[res] = Math.floor((techCostMultiplier[res] || 1) * template.cost[res]); // 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, false); this.queue.push({ "id": this.nextID++, "player": cmpPlayer.GetPlayerID(), "count": 1, "technologyTemplate": templateName, "resources": cost, "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 (let r in item.resources) { 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, true); } // 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 (var item of 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(); this.ResetAlert(); }; 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); } } } let cmpAutoGarrison; if (cmpRallyPoint) { let data = cmpRallyPoint.GetData()[0]; if (data && data.target && data.target == this.entity && data.command == "garrison") cmpAutoGarrison = Engine.QueryInterface(this.entity, IID_GarrisonHolder); } for (let i = 0; i < count; ++i) { let ent = this.entityCache[0]; let cmpNewOwnership = Engine.QueryInterface(ent, IID_Ownership); let garrisoned = false; if (cmpAutoGarrison) { // Temporary owner affectation needed for GarrisonHolder checks cmpNewOwnership.SetOwnerQuiet(cmpOwnership.GetOwner()); garrisoned = cmpAutoGarrison.PerformGarrison(ent); cmpNewOwnership.SetOwnerQuiet(-1); } if (garrisoned) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (cmpUnitAI) cmpUnitAI.Autogarrison(this.entity); } else { let pos = cmpFootprint.PickSpawnPoint(ent); if (pos.y < 0) break; let cmpNewPosition = Engine.QueryInterface(ent, IID_Position); cmpNewPosition.JumpTo(pos.x, pos.z); let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) cmpNewPosition.SetYRotation(cmpPosition.GetPosition().horizAngleTo(pos)); spawnedEnts.push(ent); } cmpNewOwnership.SetOwner(cmpOwnership.GetOwner()); 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 (var com of 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, true); } 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); + let template = TechnologyTemplates.Get(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 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/TechnologyManager.js (revision 20737) @@ -1,494 +1,481 @@ 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() { // Holds names of technologies that have been researched. this.researchedTechs = new Set(); // Maps from technolgy name to the entityID of the researcher. this.researchQueued = new Map(); // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); // This stores the modifications to unit stats from researched technologies // Example data: {"ResourceGatherer/Rates/food.grain": [ // {"multiply": 1.15, "affects": ["FemaleCitizen", "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.unresearchedAutoResearchTechs = new Set(); - var allTechs = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager).GetAllTechs(); - for (var key in allTechs) + let allTechs = TechnologyTemplates.GetAll(); + for (let key in allTechs) if (allTechs[key].autoResearch || allTechs[key].top) this.unresearchedAutoResearchTechs.add(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 cmpDataTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); for (let key of this.unresearchedAutoResearchTechs) { - var tech = cmpDataTempMan.GetTechnologyTemplate(key); + let tech = TechnologyTemplates.Get(key); if ((tech.autoResearch && this.CanResearch(key)) || (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom)))) { this.unresearchedAutoResearchTechs.delete(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_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.IsTechnologyQueued = function(tech) { return this.researchQueued.has(tech); }; TechnologyManager.prototype.IsTechnologyResearched = function(tech) { return this.researchedTechs.has(tech); }; TechnologyManager.prototype.IsTechnologyStarted = function(tech) { return this.researchStarted.has(tech); }; // Checks the requirements for a technology to see if it can be researched at the current time TechnologyManager.prototype.CanResearch = function(tech) { - let template = this.GetTechnologyTemplate(tech); + let template = TechnologyTemplates.Get(tech); if (!template) { warn("Technology \"" + tech + "\" does not exist"); 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(DeriveTechnologyRequirements(template, Engine.QueryInterface(this.entity, IID_Player).GetCiv())); }; /** * Private function for checking a set of requirements is met * @param {object} reqs - Technology requirements as derived from the technology template by globalscripts * @param {boolean} civonly - True if only the civ requirement is to be checked * * @return true if the requirements pass, false otherwise */ TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly = false) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (!reqs) return false; if (civonly || !reqs.length) return true; return reqs.some(req => { return Object.keys(req).every(type => { switch (type) { case "techs": return req[type].every(this.IsTechnologyResearched, this); case "entities": return req[type].every(this.DoesEntitySpecPass, this); } return false; }); }); }; TechnologyManager.prototype.DoesEntitySpecPass = function(entity) { switch (entity.check) { case "count": if (!this.classCounts[entity.class] || this.classCounts[entity.class] < entity.number) return false; break; case "variants": if (!this.typeCountsByClass[entity.class] || Object.keys(this.typeCountsByClass[entity.class]).length < entity.number) return false; break; } return true; }; 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, false); - var template = this.GetTechnologyTemplate(tech); - - if (!template) - { - error("Tried to research invalid technology: " + uneval(tech)); - return; - } - var modifiedComponents = {}; this.researchedTechs.add(tech); // store the modifications in an easy to access structure + let template = TechnologyTemplates.Get(tech); if (template.modifications) { let derivedModifiers = DeriveModificationsFromTech(template); for (let modifierPath in derivedModifiers) { if (!this.modifications[modifierPath]) this.modifications[modifierPath] = []; this.modifications[modifierPath] = this.modifications[modifierPath].concat(derivedModifiers[modifierPath]); let component = modifierPath.split("/")[0]; if (!modifiedComponents[component]) modifiedComponents[component] = []; modifiedComponents[component].push(modifierPath); this.modificationCache[modifierPath] = {}; } } if (template.replaces && template.replaces.length > 0) { for (var i of template.replaces) { if (!i || this.IsTechnologyResearched(i)) continue; this.researchedTechs.add(i); // 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); ents.push(this.entity); // 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]}); } if (tech.startsWith("phase") && !template.autoResearch) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [playerID], "phaseName": tech, "phaseState": "completed" }); } }; // 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) { let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) return curValue; this.modificationCache[valueName][ent] = { "origValue": curValue, "newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), 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) { if (!template || !template.Identity) return curValue; return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue); }; /** * Marks a technology as being queued for research at the given entityID. */ TechnologyManager.prototype.QueuedResearch = function(tech, researcher) { this.researchQueued.set(tech, researcher); }; // Marks a technology as actively being researched TechnologyManager.prototype.StartedResearch = function(tech, notification) { this.researchStarted.add(tech); if (notification && tech.startsWith("phase")) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGuiInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "started" }); } }; /** * Marks a technology as not being currently researched and optionally sends a GUI notification. */ TechnologyManager.prototype.StoppedResearch = function(tech, notification) { if (notification && tech.startsWith("phase") && this.researchStarted.has(tech)) { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": "phase", "players": [cmpPlayer.GetPlayerID()], "phaseName": tech, "phaseState": "aborted" }); } this.researchQueued.delete(tech); this.researchStarted.delete(tech); }; /** * Checks whether a technology is set to be researched. */ TechnologyManager.prototype.IsInProgress = function(tech) { return this.researchQueued.has(tech); }; /** * Returns the names of technologies that are currently being researched (non-queued). */ TechnologyManager.prototype.GetStartedTechs = function() { return this.researchStarted; }; /** * Gets the entity currently researching the technology. */ TechnologyManager.prototype.GetResearcher = function(tech) { return this.researchQueued.get(tech) }; /** * Called by GUIInterface for PlayerData. AI use. */ TechnologyManager.prototype.GetQueuedResearch = function() { return this.researchQueued; }; /** * Returns the names of technologies that have already been researched. */ 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 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_Auras.js (revision 20737) @@ -1,150 +1,150 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/RangeOverlayManager.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("Auras.js"); Engine.LoadComponentScript("AuraManager.js"); +global.AuraTemplates = { + "Get": name => { + let template = { + "type": name, + "affectedPlayers": ["Ally"], + "affects": ["CorrectClass"], + "modifications": [{ "value": "Component/Value", "add": 10 }], + "auraName": "name", + "auraDescription": "description" + }; + if (name == "range") + template.radius = auraRange; + return template; + } +}; + let playerID = [0, 1, 2]; let playerEnt = [10, 11, 12]; let playerState = "active"; let sourceEnt = 20; let targetEnt = 30; let auraRange = 40; let template = { "Identity" : { "Classes" : { "_string" : "CorrectClass OtherClass" } } }; function testAuras(name, test_function) { ResetState(); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": idx => playerEnt[idx], "GetNumPlayers": () => 3 }); AddMock(SYSTEM_ENTITY, IID_RangeManager, { "CreateActiveQuery": (ent, minRange, maxRange, players, iid, flags) => 1, "EnableActiveQuery": id => {}, "ResetActiveQuery": id => {}, "DisableActiveQuery": id => {}, "DestroyActiveQuery": id => {}, "GetEntityFlagMask": identifier => {}, "GetEntitiesByPlayer": id => [30, 31, 32] }); - AddMock(SYSTEM_ENTITY, IID_DataTemplateManager, { - "GetAuraTemplate": (name) => { - let template = { - "type": name, - "affectedPlayers": ["Ally"], - "affects": ["CorrectClass"], - "modifications": [{ "value": "Component/Value", "add": 10 }], - "auraName": "name", - "auraDescription": "description" - }; - if (name == "range") - template.radius = auraRange; - return template; - } - }); - AddMock(playerEnt[1], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[1], "GetState": () => playerState }); AddMock(playerEnt[2], IID_Player, { "IsAlly": id => id == playerID[1] || id == playerID[2], "IsEnemy": id => id != playerID[1] || id != playerID[2], "GetPlayerID": () => playerID[2], "GetState": () => playerState }); AddMock(targetEnt, IID_Identity, { "GetClassesList": () => ["CorrectClass", "OtherClass"] }); AddMock(sourceEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); if (name != "player" || playerEnt.indexOf(targetEnt) == -1) { AddMock(targetEnt, IID_Position, { "GetPosition2D": () => new Vector2D() }); AddMock(targetEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); } if (playerEnt.indexOf(sourceEnt) == -1) AddMock(sourceEnt, IID_Ownership, { "GetOwner": () => playerID[1] }); ConstructComponent(SYSTEM_ENTITY, "AuraManager", {}); let cmpAuras = ConstructComponent(sourceEnt, "Auras", { "_string": name }); test_function(name, cmpAuras); } testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); targetEnt = playerEnt[playerID[2]]; testAuras("player", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); targetEnt = 30; // Test the case when the aura source is a player entity. sourceEnt = 11; testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 15); }); sourceEnt = 20; testAuras("range", (name, cmpAuras) => { cmpAuras.OnRangeUpdate({ "tag": 1, "added": [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); cmpAuras.OnRangeUpdate({ "tag": 1, "added": [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrisonedUnits", (name, cmpAuras) => { cmpAuras.OnGarrisonedUnitsChanged({ "added" : [targetEnt], "removed": [] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.OnGarrisonedUnitsChanged({ "added" : [], "removed": [targetEnt] }); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("garrison", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasGarrisonAura(), true); cmpAuras.ApplyGarrisonBonus(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveGarrisonBonus(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 1, targetEnt), 1); }); testAuras("formation", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasFormationAura(), true); cmpAuras.ApplyFormationBonus([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); cmpAuras.RemoveFormationBonus([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); playerState = "defeated"; testAuras("global", (name, cmpAuras) => { TS_ASSERT_EQUALS(ApplyValueModificationsToTemplate("Component/Value", 5, playerID[1], template), 5); }); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js (revision 20737) @@ -1,142 +1,141 @@ Resources = { "BuildSchema": (a, b) => {} }; Engine.LoadHelperScript("Player.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); Engine.LoadComponentScript("ProductionQueue.js"); +global.TechnologyTemplates = { + "Has": name => name == "phase_town_athen" || name == "phase_city_athen", + "Get": () => ({}) +}; + const productionQueueId = 6; const playerId = 1; const playerEntityID = 2; AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true, "GetTemplate": name => ({}) }); let cmpProductionQueue = ConstructComponent(productionQueueId, "ProductionQueue", { "Entities": { "_string": "units/{civ}_cavalry_javelinist_b " + "units/{civ}_infantry_swordsman_b " + "units/{native}_support_female_citizen" }, "Technologies": { "_string": "gather_fishing_net " + "phase_town_{civ} " + "phase_city_{civ}" } }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), []); AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": id => playerEntityID }); -AddMock(SYSTEM_ENTITY, IID_DataTemplateManager, { - "TechnologyExists": name => name == "phase_town_athen" || name == "phase_city_athen" -}); - AddMock(playerEntityID, IID_Player, { "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}), "GetDisabledTemplates": () => ({}), "GetPlayerID": () => playerId }); AddMock(playerEntityID, IID_TechnologyManager, { "CheckTechnologyRequirements": () => true, - "GetTechnologyTemplate": tech => tech, "IsInProgress": () => false, "IsTechnologyResearched": () => false }); AddMock(productionQueueId, IID_Ownership, { "GetOwner": () => playerId }); AddMock(productionQueueId, IID_Identity, { "GetCiv": () => "iber" }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetEntitiesList(), ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] ); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetTechnologiesList(), ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] ); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": name => name == "units/iber_support_female_citizen", "GetTemplate": name => ({}) }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetEntitiesList(), ["units/iber_support_female_citizen"]); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { "TemplateExists": () => true, "GetTemplate": name => ({}) }); AddMock(playerEntityID, IID_Player, { "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}), "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), "GetPlayerID": () => playerId }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetEntitiesList(), ["units/iber_cavalry_javelinist_b", "units/iber_infantry_swordsman_b", "units/iber_support_female_citizen"] ); AddMock(playerEntityID, IID_Player, { "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}), "GetDisabledTemplates": () => ({ "units/iber_infantry_swordsman_b": true }), "GetPlayerID": () => playerId }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetEntitiesList(), ["units/iber_cavalry_javelinist_b", "units/iber_support_female_citizen"] ); AddMock(playerEntityID, IID_Player, { "GetCiv": () => "athen", "GetDisabledTechnologies": () => ({ "gather_fishing_net": true }), "GetDisabledTemplates": () => ({ "units/athen_infantry_swordsman_b": true }), "GetPlayerID": () => playerId }); cmpProductionQueue.CalculateEntitiesList(); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetEntitiesList(), ["units/athen_cavalry_javelinist_b", "units/iber_support_female_citizen"] ); TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), ["phase_town_athen", "phase_city_athen"] ); AddMock(playerEntityID, IID_TechnologyManager, { "CheckTechnologyRequirements": () => true, - "GetTechnologyTemplate": tech => tech, "IsInProgress": () => false, "IsTechnologyResearched": tech => tech == "phase_town_athen" }); TS_ASSERT_UNEVAL_EQUALS(cmpProductionQueue.GetTechnologiesList(), [undefined, "phase_city_athen"]); AddMock(playerEntityID, IID_Player, { "GetCiv": () => "iber", "GetDisabledTechnologies": () => ({}), "GetPlayerID": () => playerId }); TS_ASSERT_UNEVAL_EQUALS( cmpProductionQueue.GetTechnologiesList(), ["gather_fishing_net", "phase_town_generic", "phase_city_generic"] ); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_TechnologyManager.js (revision 20737) @@ -1,18 +1,18 @@ Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Engine.LoadComponentScript("TechnologyManager.js"); - AddMock(SYSTEM_ENTITY, IID_DataTemplateManager, { - "GetAllTechs": () => {} - }); +global.TechnologyTemplates = { + "GetAll": () => [] +}; let cmpTechnologyManager = ConstructComponent(SYSTEM_ENTITY, "TechnologyManager", {}); // Test CheckTechnologyRequirements let template = { "requirements": { "all": [{ "entity": { "class": "Village", "number": 5 } }, { "civ": "athen" }] } }; cmpTechnologyManager.classCounts["Village"] = 2; TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), false); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen"), true), true); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur"), true), false); cmpTechnologyManager.classCounts["Village"] = 6; TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "athen")), true); TS_ASSERT_EQUALS(cmpTechnologyManager.CheckTechnologyRequirements(DeriveTechnologyRequirements(template, "maur")), false); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 20736) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Cheat.js (revision 20737) @@ -1,177 +1,170 @@ 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": cmpPlayer = QueryPlayerIDInterface(input.parameter); if (cmpPlayer) cmpPlayer.SetState("defeated", markForTranslation("%(player)s has been defeated (cheat).")); 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 cmpDataTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager); - if (cmpDataTemplateManager.ListAllTechs().indexOf(parameter + "_" + cmpPlayer.civ) > -1) + if (TechnologyTemplates.Has(parameter + "_" + cmpPlayer.civ)) parameter += "_" + cmpPlayer.civ; else parameter += "_generic"; 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)) + if (TechnologyTemplates.Has(techname) && + !cmpTechnologyManager.IsTechnologyResearched(techname)) cmpTechnologyManager.ResearchTechnology(techname); return; case "metaCheat": for (let resource of Resources.GetCodes()) 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; case "playRetro": let play = input.parameter.toLowerCase() != "off"; cmpGuiInterface.PushNotification({ "type": "play-tracks", "tracks": play && input.parameter.split(" "), "lock": play, "players": [input.player] }); return; default: warn("Cheat '" + input.action + "' is not implemented"); return; } } Engine.RegisterGlobal("Cheat", Cheat); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/ModificationTemplates.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/ModificationTemplates.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/ModificationTemplates.js (revision 20737) @@ -0,0 +1,2 @@ +// Loads the Aura and Technology JSON files +LoadModificationTemplates(); Property changes on: ps/trunk/binaries/data/mods/public/simulation/helpers/ModificationTemplates.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 20736) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 20737) @@ -1,289 +1,291 @@ /* Copyright (C) 2017 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 "MapGenerator.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" // TODO: what's a good default? perhaps based on map size #define RMS_RUNTIME_SIZE 96 * 1024 * 1024 CMapGeneratorWorker::CMapGeneratorWorker() { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end pthread_join(m_WorkerThread, NULL); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { CScopeLock lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } void* CMapGeneratorWorker::RunThread(void *data) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); CMapGeneratorWorker* self = static_cast(data); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", ScriptInterface::CreateRuntime(g_ScriptRuntime, RMS_RUNTIME_SIZE)); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 CScopeLock lock(self->m_WorkerMutex); self->m_Progress = -1; } // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. return NULL; } bool CMapGeneratorWorker::Run() { // We must destroy the ScriptInterface in the same thread because the JSAPI requires that! // Also we must not be in a request when calling the ScriptInterface destructor, so the autoFree object // must be instantiated before the request (destructors are called in reverse order of instantiation) struct AutoFree { AutoFree(ScriptInterface* p) : m_p(p) {} ~AutoFree() { SAFE_DELETE(m_p); } ScriptInterface* m_p; } autoFree(m_ScriptInterface); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); m_ScriptInterface->SetCallbackData(static_cast (this)); // Replace RNG with a seeded deterministic function m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); - m_ScriptInterface->LoadGlobalScripts(); // Functions for RMS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("ExportMap"); m_ScriptInterface->RegisterFunction("SetProgress"); m_ScriptInterface->RegisterFunction("GetTemplate"); m_ScriptInterface->RegisterFunction("TemplateExists"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates"); m_ScriptInterface->RegisterFunction("GetTerrainTileSize"); + // Globalscripts may use VFS script functions + m_ScriptInterface->LoadGlobalScripts(); + // Parse settings JS::RootedValue settingsVal(cx); if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") || !m_ScriptInterface->GetProperty(settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); m_MapGenRNG.seed(seed); // Copy settings to global variable JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } int CMapGeneratorWorker::GetProgress() { CScopeLock lock(m_WorkerMutex); return m_Progress; } shared_ptr CMapGeneratorWorker::GetResults() { CScopeLock lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy results CScopeLock lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy data CScopeLock lock(self->m_WorkerMutex); self->m_Progress = progress; } CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } int CMapGeneratorWorker::GetTerrainTileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return TERRAIN_TILE_SIZE; } bool CMapGeneratorWorker::LoadScripts(const std::wstring& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = L"maps/random/" + libraryName + L"/"; VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker()) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } shared_ptr CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/simulation2/TypeList.h =================================================================== --- ps/trunk/source/simulation2/TypeList.h (revision 20736) +++ ps/trunk/source/simulation2/TypeList.h (revision 20737) @@ -1,202 +1,199 @@ /* Copyright (C) 2017 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(VisionSharingChanged) MESSAGE(MinimapPing) MESSAGE(CinemaPathEnded) MESSAGE(CinemaQueueEnded) MESSAGE(PlayerColorChanged) // 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(RangeOverlayRenderer) COMPONENT(RangeOverlayRenderer) INTERFACE(Selectable) COMPONENT(Selectable) INTERFACE(Settlement) COMPONENT(SettlementScripted) INTERFACE(SoundManager) COMPONENT(SoundManager) INTERFACE(ValueModificationManager) COMPONENT(ValueModificationManagerScripted) -INTERFACE(DataTemplateManager) -COMPONENT(DataTemplateManagerScripted) - INTERFACE(Terrain) COMPONENT(Terrain) INTERFACE(TerritoryDecayManager) COMPONENT(TerritoryDecayManagerScripted) 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 20736) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 20737) @@ -1,1220 +1,1189 @@ /* Copyright (C) 2017 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/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TemplateLoader.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/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" extern void kill_mainloop(); /** * @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, const std::wstring& behavior, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), 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); m_ScriptInterface->SetProperty(settings, "behavior", m_Behavior, false); if (!m_UseSharedComponent) { 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; std::wstring m_Behavior; 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("Exit"); m_ScriptInterface->RegisterFunction("ComputePath"); m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); m_ScriptInterface->RegisterFunction("GetTemplate"); JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get())); + + // Globalscripts may use VFS script functions + m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetJSRuntime(), Trace, this); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } 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()); JSAutoRequest rq(cx); 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 CParamNode GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name) { ENSURE(pCxPrivate->pCBData); CAIWorker* self = static_cast (pCxPrivate->pCBData); return self->GetTemplate(name); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } static void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { kill_mainloop(); } /** * 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) + bool TryLoadSharedComponent() { 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, const std::wstring& behavior) { shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, 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 UpdateGameState(const shared_ptr& gameState) { ENSURE(m_CommandsComputed); m_GameState = gameState; } void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { ENSURE(m_CommandsComputed); bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H; m_PassabilityMap = passabilityMap; if (globallyDirty) m_LongPathfinder.Reload(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); else m_LongPathfinder.Update(&m_PassabilityMap, dirtinessGrid); JSContext* cx = m_ScriptInterface->GetContext(); if (dimensionChange || justDeserialized) ScriptInterface::ToJSVal(cx, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JSAutoRequest rq(cx); JS::RootedObject mapObj(cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(cx); ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(cx, &mapData.toObject()); u32 length = 0; ENSURE(JS_GetArrayLength(cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, nogc), m_PassabilityMap.m_Data, nbytes); } } void UpdateTerritoryMap(const Grid& territoryMap) { ENSURE(m_CommandsComputed); bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H; m_TerritoryMap = territoryMap; JSContext* cx = m_ScriptInterface->GetContext(); if (dimensionChange) ScriptInterface::ToJSVal(cx, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JSAutoRequest rq(cx); JS::RootedObject mapObj(cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(cx); ENSURE(JS_GetProperty(cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(cx, &mapData.toObject()); u32 length = 0; ENSURE(JS_GetArrayLength(cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, nogc), m_TerritoryMap.m_Data, nbytes); } } void StartComputation() { 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); } } 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) { if (m_Players.empty()) return; 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.String("behavior", m_Players[i]->m_Behavior, 1, 256); 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) { m_PlayerMetadata.clear(); m_Players.clear(); if (numAis == 0) return; 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); 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); - + TryLoadSharedComponent(); 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; std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); deserializer.String("behavior", behavior, 1, 256); if (!AddPlayer(name, player, difficulty, behavior)) 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!"); JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, &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(cx, 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_CallObjectTracer(trc, &prototype.second, "CAIWorker::m_DeserializablePrototypes"); for (std::pair>& metadata : m_PlayerMetadata) JS_CallValueTracer(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; CTemplateLoader m_TemplateLoader; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } 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) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { LoadUsedEntityTemplates(); m_Worker.AddPlayer(id, player, difficulty, behavior); // 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() { - const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); - - // load the technology templates - CmpPtr cmpDataTemplateManager(GetSystemEntity()); - ENSURE(cmpDataTemplateManager); - - JS::RootedValue techTemplates(cx); - cmpDataTemplateManager->GetAllTechs(&techTemplates); - m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates)); - - m_Worker.TryLoadSharedComponent(true); + m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { const 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, &m_TerritoriesDirtyBlinkingID)) 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"); const 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); LoadPathfinderClasses(state); // add the pathfinding classes to it // Update the game state m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdate first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized)) { const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid(); m_Worker.UpdateTerritoryMap(territoryMap); } m_Worker.StartComputation(); m_JustDeserialized = false; } virtual void PushCommands() { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; const 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: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const 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/system/ComponentManager.cpp =================================================================== --- ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 20736) +++ ps/trunk/source/simulation2/system/ComponentManager.cpp (revision 20737) @@ -1,1185 +1,1187 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ComponentManager.h" #include "DynamicSubscription.h" #include "IComponent.h" #include "ParamNode.h" #include "SimContext.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpTemplateManager.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/scripting/JSInterface_VFS.h" /** * Used for script-only message types. */ class CMessageScripted : public CMessage { public: virtual int GetType() const { return mtid; } virtual const char* GetScriptHandlerName() const { return handlerName.c_str(); } virtual const char* GetScriptGlobalHandlerName() const { return globalHandlerName.c_str(); } virtual JS::Value ToJSVal(const ScriptInterface& UNUSED(scriptInterface)) const { return msg.get(); } CMessageScripted(const ScriptInterface& scriptInterface, int mtid, const std::string& name, JS::HandleValue msg) : mtid(mtid), handlerName("On" + name), globalHandlerName("OnGlobal" + name), msg(scriptInterface.GetJSRuntime(), msg) { } int mtid; std::string handlerName; std::string globalHandlerName; JS::PersistentRootedValue msg; }; CComponentManager::CComponentManager(CSimContext& context, shared_ptr rt, bool skipScriptFunctions) : m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine", "Simulation", rt), m_SimContext(context), m_CurrentlyHotloading(false) { context.SetComponentManager(this); m_ScriptInterface.SetCallbackData(static_cast (this)); 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 if (!skipScriptFunctions) { JSI_VFS::RegisterScriptFunctions_Simulation(m_ScriptInterface); m_ScriptInterface.RegisterFunction ("RegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterSystemComponentType"); m_ScriptInterface.RegisterFunction ("ReRegisterComponentType"); m_ScriptInterface.RegisterFunction ("RegisterInterface"); m_ScriptInterface.RegisterFunction ("RegisterMessageType"); m_ScriptInterface.RegisterFunction ("RegisterGlobal"); m_ScriptInterface.RegisterFunction ("QueryInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetEntitiesWithInterface> ("GetEntitiesWithInterface"); m_ScriptInterface.RegisterFunction, int, CComponentManager::Script_GetComponentsWithInterface> ("GetComponentsWithInterface"); m_ScriptInterface.RegisterFunction ("PostMessage"); m_ScriptInterface.RegisterFunction ("BroadcastMessage"); m_ScriptInterface.RegisterFunction ("AddEntity"); m_ScriptInterface.RegisterFunction ("AddLocalEntity"); m_ScriptInterface.RegisterFunction ("DestroyEntity"); m_ScriptInterface.RegisterFunction ("FlushDestroyedEntities"); } + // Globalscripts may use VFS script functions + m_ScriptInterface.LoadGlobalScripts(); + // Define MT_*, IID_* as script globals, and store their names #define MESSAGE(name) m_ScriptInterface.SetGlobal("MT_" #name, (int)MT_##name); #define INTERFACE(name) \ m_ScriptInterface.SetGlobal("IID_" #name, (int)IID_##name); \ m_InterfaceIdsByName[#name] = IID_##name; #define COMPONENT(name) #include "simulation2/TypeList.h" #undef MESSAGE #undef INTERFACE #undef COMPONENT m_ScriptInterface.SetGlobal("INVALID_ENTITY", (int)INVALID_ENTITY); m_ScriptInterface.SetGlobal("SYSTEM_ENTITY", (int)SYSTEM_ENTITY); m_ComponentsByInterface.resize(IID__LastNative); ResetState(); } CComponentManager::~CComponentManager() { ResetState(); } void CComponentManager::LoadComponentTypes() { #define MESSAGE(name) \ RegisterMessageType(MT_##name, #name); #define INTERFACE(name) \ extern void RegisterComponentInterface_##name(ScriptInterface&); \ RegisterComponentInterface_##name(m_ScriptInterface); #define COMPONENT(name) \ extern void RegisterComponentType_##name(CComponentManager&); \ m_CurrentComponent = CID_##name; \ RegisterComponentType_##name(*this); #include "simulation2/TypeList.h" m_CurrentComponent = CID__Invalid; #undef MESSAGE #undef INTERFACE #undef COMPONENT } bool CComponentManager::LoadScript(const VfsPath& filename, bool hotload) { m_CurrentlyHotloading = hotload; CVFSFile file; PSRETURN loadOk = file.Load(g_VFS, filename); if (loadOk != PSRETURN_OK) // VFS will log the failed file and the reason return false; std::string content = file.DecodeUTF8(); // assume it's UTF-8 bool ok = m_ScriptInterface.LoadScript(filename, content); return ok; } void CComponentManager::Script_RegisterComponentType_Common(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor, bool reRegister, bool systemComponent) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); JSContext* cx = componentManager->m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); // Find the C++ component that wraps the interface int cidWrapper = componentManager->GetScriptWrapper(iid); if (cidWrapper == CID__Invalid) { componentManager->m_ScriptInterface.ReportError("Invalid interface id"); return; } const ComponentType& ctWrapper = componentManager->m_ComponentTypesById[cidWrapper]; bool mustReloadComponents = false; // for hotloading ComponentTypeId cid = componentManager->LookupCID(cname); if (cid == CID__Invalid) { if (reRegister) { std::string msg("ReRegistering component type that was not registered before '"+cname+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } // Allocate a new cid number cid = componentManager->m_NextScriptComponentTypeId++; componentManager->m_ComponentTypeIdsByName[cname] = cid; if (systemComponent) componentManager->MarkScriptedComponentForSystemEntity(cid); } else { // Component type is already loaded, so do hotloading: if (!componentManager->m_CurrentlyHotloading && !reRegister) { std::string msg("Registering component type with already-registered name '"+cname+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } const ComponentType& ctPrevious = componentManager->m_ComponentTypesById[cid]; // We can only replace scripted component types, not native ones if (ctPrevious.type != CT_Script) { std::string msg("Loading script component type with same name '"+cname+"' as native component"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } // We don't support changing the IID of a component type (it would require fiddling // around with m_ComponentsByInterface and being careful to guarantee uniqueness per entity) if (ctPrevious.iid != iid) { // ...though it only matters if any components exist with this type if (!componentManager->m_ComponentsByTypeId[cid].empty()) { componentManager->m_ScriptInterface.ReportError("Hotloading script component type mustn't change interface ID"); return; } } // Remove the old component type's message subscriptions std::map >::iterator it; for (it = componentManager->m_LocalMessageSubscriptions.begin(); it != componentManager->m_LocalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } for (it = componentManager->m_GlobalMessageSubscriptions.begin(); it != componentManager->m_GlobalMessageSubscriptions.end(); ++it) { std::vector& types = it->second; std::vector::iterator ctit = find(types.begin(), types.end(), cid); if (ctit != types.end()) types.erase(ctit); } mustReloadComponents = true; } std::string schema = ""; { JS::RootedValue prototype(cx); if (componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &prototype) && componentManager->m_ScriptInterface.HasProperty(prototype, "Schema")) { componentManager->m_ScriptInterface.GetProperty(prototype, "Schema", schema); } } // Construct a new ComponentType, using the wrapper's alloc functions ComponentType ct( CT_Script, iid, ctWrapper.alloc, ctWrapper.dealloc, cname, schema, DefPersistentRooted(cx, ctor) ); componentManager->m_ComponentTypesById[cid] = std::move(ct); componentManager->m_CurrentComponent = cid; // needed by Subscribe // Find all the ctor prototype's On* methods, and subscribe to the appropriate messages: JS::RootedValue protoVal(cx); if (!componentManager->m_ScriptInterface.GetProperty(ctor, "prototype", &protoVal)) return; // error std::vector methods; JS::RootedObject proto(cx); if (!protoVal.isObjectOrNull()) return; // error proto = protoVal.toObjectOrNull(); if (!componentManager->m_ScriptInterface.EnumeratePropertyNamesWithPrefix(protoVal, "On", methods)) return; // error for (std::vector::const_iterator it = methods.begin(); it != methods.end(); ++it) { std::string name = (*it).substr(2); // strip the "On" prefix // Handle "OnGlobalFoo" functions specially bool isGlobal = false; if (name.substr(0, 6) == "Global") { isGlobal = true; name = name.substr(6); } std::map::const_iterator mit = componentManager->m_MessageTypeIdsByName.find(name); if (mit == componentManager->m_MessageTypeIdsByName.end()) { std::string msg("Registered component has unrecognised '" + *it + "' message handler method"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); return; } if (isGlobal) componentManager->SubscribeGloballyToMessageType(mit->second); else componentManager->SubscribeToMessageType(mit->second); } componentManager->m_CurrentComponent = CID__Invalid; if (mustReloadComponents) { // For every script component with this cid, we need to switch its // prototype from the old constructor's prototype property to the new one's const std::map& comps = componentManager->m_ComponentsByTypeId[cid]; std::map::const_iterator eit = comps.begin(); for (; eit != comps.end(); ++eit) { JS::RootedValue instance(cx, eit->second->GetJSInstance()); if (!instance.isNull()) { componentManager->m_ScriptInterface.SetPrototype(instance, protoVal); } } } } void CComponentManager::Script_RegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, false); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_RegisterSystemComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, false, true); componentManager->m_ScriptInterface.SetGlobal(cname.c_str(), ctor, componentManager->m_CurrentlyHotloading); } void CComponentManager::Script_ReRegisterComponentType(ScriptInterface::CxPrivate* pCxPrivate, int iid, const std::string& cname, JS::HandleValue ctor) { Script_RegisterComponentType_Common(pCxPrivate, iid, cname, ctor, true, false); } void CComponentManager::Script_RegisterInterface(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::map::iterator it = componentManager->m_InterfaceIdsByName.find(name); if (it != componentManager->m_InterfaceIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) { std::string msg("Registering interface with already-registered name '"+name+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); } return; } // IIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_InterfaceIdsByName.size() + 1; componentManager->m_InterfaceIdsByName[name] = (InterfaceId)id; componentManager->m_ComponentsByInterface.resize(id+1); // add one so we can index by InterfaceId componentManager->m_ScriptInterface.SetGlobal(("IID_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterMessageType(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::map::iterator it = componentManager->m_MessageTypeIdsByName.find(name); if (it != componentManager->m_MessageTypeIdsByName.end()) { // Redefinitions are fine (and just get ignored) when hotloading; otherwise // they're probably unintentional and should be reported if (!componentManager->m_CurrentlyHotloading) { std::string msg("Registering message type with already-registered name '"+name+"'"); componentManager->m_ScriptInterface.ReportError(msg.c_str()); } return; } // MTIDs start at 1, so size+1 is the next unused one size_t id = componentManager->m_MessageTypeIdsByName.size() + 1; componentManager->RegisterMessageType((MessageTypeId)id, name.c_str()); componentManager->m_ScriptInterface.SetGlobal(("MT_" + name).c_str(), (int)id); } void CComponentManager::Script_RegisterGlobal(ScriptInterface::CxPrivate* pCxPrivate, const std::string& name, JS::HandleValue value) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); // Set the value, and accept duplicates only if hotloading (otherwise it's an error, // in order to detect accidental duplicate definitions of globals) componentManager->m_ScriptInterface.SetGlobal(name.c_str(), value, componentManager->m_CurrentlyHotloading); } IComponent* CComponentManager::Script_QueryInterface(ScriptInterface::CxPrivate* pCxPrivate, int ent, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); IComponent* component = componentManager->QueryInterface((entity_id_t)ent, iid); return component; } std::vector CComponentManager::Script_GetEntitiesWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::vector ret; const InterfaceListUnordered& ents = componentManager->GetEntitiesWithInterfaceUnordered(iid); for (InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) if (!ENTITY_IS_LOCAL(it->first)) ret.push_back(it->first); std::sort(ret.begin(), ret.end()); return ret; } std::vector CComponentManager::Script_GetComponentsWithInterface(ScriptInterface::CxPrivate* pCxPrivate, int iid) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::vector ret; InterfaceList ents = componentManager->GetEntitiesWithInterface(iid); for (InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) ret.push_back(it->second); // TODO: maybe we should exclude local entities return ret; } CMessage* CComponentManager::ConstructMessage(int mtid, JS::HandleValue data) { if (mtid == MT__Invalid || mtid > (int)m_MessageTypeIdsByName.size()) // (IDs start at 1 so use '>' here) LOGERROR("PostMessage with invalid message type ID '%d'", mtid); if (mtid < MT__LastNative) { return CMessageFromJSVal(mtid, m_ScriptInterface, data); } else { return new CMessageScripted(m_ScriptInterface, mtid, m_MessageTypeNamesById[mtid], data); } } void CComponentManager::Script_PostMessage(ScriptInterface::CxPrivate* pCxPrivate, int ent, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->PostMessage(ent, *msg); delete msg; } void CComponentManager::Script_BroadcastMessage(ScriptInterface::CxPrivate* pCxPrivate, int mtid, JS::HandleValue data) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); CMessage* msg = componentManager->ConstructMessage(mtid, data); if (!msg) return; // error componentManager->BroadcastMessage(*msg); delete msg; } int CComponentManager::Script_AddEntity(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewEntity()); return (int)ent; } int CComponentManager::Script_AddLocalEntity(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); std::wstring name(templateName.begin(), templateName.end()); // TODO: should validate the string to make sure it doesn't contain scary characters // that will let it access non-component-template files entity_id_t ent = componentManager->AddEntity(name, componentManager->AllocateNewLocalEntity()); return (int)ent; } void CComponentManager::Script_DestroyEntity(ScriptInterface::CxPrivate* pCxPrivate, int ent) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->DestroyComponentsSoon(ent); } void CComponentManager::Script_FlushDestroyedEntities(ScriptInterface::CxPrivate *pCxPrivate) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); componentManager->FlushDestroyedComponents(); } void CComponentManager::ResetState() { // Delete all dynamic message subscriptions m_DynamicMessageSubscriptionsNonsync.clear(); m_DynamicMessageSubscriptionsNonsyncByComponent.clear(); // Delete all IComponents std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.begin(); for (; eit != iit->second.end(); ++eit) { eit->second->Deinit(); m_ComponentTypesById[iit->first].dealloc(eit->second); } } std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) ifcit->clear(); m_ComponentsByTypeId.clear(); // Delete all SEntityComponentCaches std::unordered_map::iterator ccit = m_ComponentCaches.begin(); for (; ccit != m_ComponentCaches.end(); ++ccit) free(ccit->second); m_ComponentCaches.clear(); m_SystemEntity = CEntityHandle(); m_DestructionQueue.clear(); // Reset IDs m_NextEntityId = SYSTEM_ENTITY + 1; m_NextLocalEntityId = FIRST_LOCAL_ENTITY; } void CComponentManager::SetRNGSeed(u32 seed) { m_RNG.seed(seed); } void CComponentManager::RegisterComponentType(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c(CT_Native, iid, alloc, dealloc, name, schema, DefPersistentRooted()); m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; } void CComponentManager::RegisterComponentTypeScriptWrapper(InterfaceId iid, ComponentTypeId cid, AllocFunc alloc, DeallocFunc dealloc, const char* name, const std::string& schema) { ComponentType c(CT_ScriptWrapper, iid, alloc, dealloc, name, schema, DefPersistentRooted()); m_ComponentTypesById.insert(std::make_pair(cid, std::move(c))); m_ComponentTypeIdsByName[name] = cid; // TODO: merge with RegisterComponentType } void CComponentManager::MarkScriptedComponentForSystemEntity(CComponentManager::ComponentTypeId cid) { m_ScriptedSystemComponents.push_back(cid); } void CComponentManager::RegisterMessageType(MessageTypeId mtid, const char* name) { m_MessageTypeIdsByName[name] = mtid; m_MessageTypeNamesById[mtid] = name; } void CComponentManager::SubscribeToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_LocalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::SubscribeGloballyToMessageType(MessageTypeId mtid) { // TODO: verify mtid ENSURE(m_CurrentComponent != CID__Invalid); std::vector& types = m_GlobalMessageSubscriptions[mtid]; types.push_back(m_CurrentComponent); std::sort(types.begin(), types.end()); // TODO: just sort once at the end of LoadComponents } void CComponentManager::FlattenDynamicSubscriptions() { std::map::iterator it; for (it = m_DynamicMessageSubscriptionsNonsync.begin(); it != m_DynamicMessageSubscriptionsNonsync.end(); ++it) { it->second.Flatten(); } } void CComponentManager::DynamicSubscriptionNonsync(MessageTypeId mtid, IComponent* component, bool enable) { if (enable) { bool newlyInserted = m_DynamicMessageSubscriptionsNonsyncByComponent[component].insert(mtid).second; if (newlyInserted) m_DynamicMessageSubscriptionsNonsync[mtid].Add(component); } else { size_t numRemoved = m_DynamicMessageSubscriptionsNonsyncByComponent[component].erase(mtid); if (numRemoved) m_DynamicMessageSubscriptionsNonsync[mtid].Remove(component); } } void CComponentManager::RemoveComponentDynamicSubscriptions(IComponent* component) { std::map >::iterator it = m_DynamicMessageSubscriptionsNonsyncByComponent.find(component); if (it == m_DynamicMessageSubscriptionsNonsyncByComponent.end()) return; std::set::iterator mtit; for (mtit = it->second.begin(); mtit != it->second.end(); ++mtit) { m_DynamicMessageSubscriptionsNonsync[*mtit].Remove(component); // Need to flatten the subscription lists immediately to avoid dangling IComponent* references m_DynamicMessageSubscriptionsNonsync[*mtit].Flatten(); } m_DynamicMessageSubscriptionsNonsyncByComponent.erase(it); } CComponentManager::ComponentTypeId CComponentManager::LookupCID(const std::string& cname) const { std::map::const_iterator it = m_ComponentTypeIdsByName.find(cname); if (it == m_ComponentTypeIdsByName.end()) return CID__Invalid; return it->second; } std::string CComponentManager::LookupComponentTypeName(ComponentTypeId cid) const { std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) return ""; return it->second.name; } CComponentManager::ComponentTypeId CComponentManager::GetScriptWrapper(InterfaceId iid) { if (iid >= IID__LastNative && iid <= (int)m_InterfaceIdsByName.size()) // use <= since IDs start at 1 return CID_UnknownScript; std::map::const_iterator it = m_ComponentTypesById.begin(); for (; it != m_ComponentTypesById.end(); ++it) if (it->second.iid == iid && it->second.type == CT_ScriptWrapper) return it->first; std::map::const_iterator iiit = m_InterfaceIdsByName.begin(); for (; iiit != m_InterfaceIdsByName.end(); ++iiit) if (iiit->second == iid) { LOGERROR("No script wrapper found for interface id %d '%s'", iid, iiit->first.c_str()); return CID__Invalid; } LOGERROR("No script wrapper found for interface id %d", iid); return CID__Invalid; } entity_id_t CComponentManager::AllocateNewEntity() { entity_id_t id = m_NextEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewLocalEntity() { entity_id_t id = m_NextLocalEntityId++; // TODO: check for overflow return id; } entity_id_t CComponentManager::AllocateNewEntity(entity_id_t preferredId) { // TODO: ensure this ID hasn't been allocated before // (this might occur with broken map files) // Trying to actually add two entities with the same id will fail in AddEntitiy entity_id_t id = preferredId; // Ensure this ID won't be allocated again if (id >= m_NextEntityId) m_NextEntityId = id+1; // TODO: check for overflow return id; } bool CComponentManager::AddComponent(CEntityHandle ent, ComponentTypeId cid, const CParamNode& paramNode) { IComponent* component = ConstructComponent(ent, cid); if (!component) return false; component->Init(paramNode); return true; } void CComponentManager::AddSystemComponents(bool skipScriptedComponents, bool skipAI) { CParamNode noParam; AddComponent(m_SystemEntity, CID_TemplateManager, noParam); AddComponent(m_SystemEntity, CID_CinemaManager, noParam); AddComponent(m_SystemEntity, CID_CommandQueue, noParam); AddComponent(m_SystemEntity, CID_ObstructionManager, noParam); AddComponent(m_SystemEntity, CID_ParticleManager, noParam); AddComponent(m_SystemEntity, CID_Pathfinder, noParam); AddComponent(m_SystemEntity, CID_ProjectileManager, noParam); AddComponent(m_SystemEntity, CID_RangeManager, noParam); AddComponent(m_SystemEntity, CID_SoundManager, noParam); AddComponent(m_SystemEntity, CID_Terrain, noParam); AddComponent(m_SystemEntity, CID_TerritoryManager, noParam); AddComponent(m_SystemEntity, CID_UnitRenderer, noParam); AddComponent(m_SystemEntity, CID_WaterManager, noParam); // Add scripted system components: if (!skipScriptedComponents) { for (uint32_t i = 0; i < m_ScriptedSystemComponents.size(); ++i) AddComponent(m_SystemEntity, m_ScriptedSystemComponents[i], noParam); if (!skipAI) AddComponent(m_SystemEntity, CID_AIManager, noParam); } } IComponent* CComponentManager::ConstructComponent(CEntityHandle ent, ComponentTypeId cid) { JSContext* cx = m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); std::map::const_iterator it = m_ComponentTypesById.find(cid); if (it == m_ComponentTypesById.end()) { LOGERROR("Invalid component id %d", cid); return NULL; } const ComponentType& ct = it->second; ENSURE((size_t)ct.iid < m_ComponentsByInterface.size()); boost::unordered_map& emap1 = m_ComponentsByInterface[ct.iid]; if (emap1.find(ent.GetId()) != emap1.end()) { LOGERROR("Multiple components for interface %d", ct.iid); return NULL; } std::map& emap2 = m_ComponentsByTypeId[cid]; // If this is a scripted component, construct the appropriate JS object first JS::RootedValue obj(cx); if (ct.type == CT_Script) { m_ScriptInterface.CallConstructor(ct.ctor.get(), JS::HandleValueArray::empty(), &obj); if (obj.isNull()) { LOGERROR("Script component constructor failed"); return NULL; } } // Construct the new component IComponent* component = ct.alloc(m_ScriptInterface, obj); ENSURE(component); component->SetEntityHandle(ent); component->SetSimContext(m_SimContext); // Store a reference to the new component emap1.insert(std::make_pair(ent.GetId(), component)); emap2.insert(std::make_pair(ent.GetId(), component)); // TODO: We need to more careful about this - if an entity is constructed by a component // while we're iterating over all components, this will invalidate the iterators and everything // will break. // We probably need some kind of delayed addition, so they get pushed onto a queue and then // inserted into the world later on. (Be careful about immediation deletion in that case, too.) SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && ct.iid < (int)cache->numInterfaces && cache->interfaces[ct.iid] == NULL); cache->interfaces[ct.iid] = component; return component; } void CComponentManager::AddMockComponent(CEntityHandle ent, InterfaceId iid, IComponent& component) { // Just add it into the by-interface map, not the by-component-type map, // so it won't be considered for messages or deletion etc boost::unordered_map& emap1 = m_ComponentsByInterface.at(iid); if (emap1.find(ent.GetId()) != emap1.end()) debug_warn(L"Multiple components for interface"); emap1.insert(std::make_pair(ent.GetId(), &component)); SEntityComponentCache* cache = ent.GetComponentCache(); ENSURE(cache != NULL && iid < (int)cache->numInterfaces && cache->interfaces[iid] == NULL); cache->interfaces[iid] = &component; } CEntityHandle CComponentManager::AllocateEntityHandle(entity_id_t ent) { // Interface IDs start at 1, and SEntityComponentCache is defined with a 1-sized array, // so we need space for an extra m_InterfaceIdsByName.size() items SEntityComponentCache* cache = (SEntityComponentCache*)calloc(1, sizeof(SEntityComponentCache) + sizeof(IComponent*) * m_InterfaceIdsByName.size()); ENSURE(cache != NULL); cache->numInterfaces = m_InterfaceIdsByName.size() + 1; ENSURE(m_ComponentCaches.find(ent) == m_ComponentCaches.end()); m_ComponentCaches[ent] = cache; return CEntityHandle(ent, cache); } CEntityHandle CComponentManager::LookupEntityHandle(entity_id_t ent, bool allowCreate) { std::unordered_map::iterator it; it = m_ComponentCaches.find(ent); if (it == m_ComponentCaches.end()) { if (allowCreate) return AllocateEntityHandle(ent); else return CEntityHandle(ent, NULL); } else return CEntityHandle(ent, it->second); } void CComponentManager::InitSystemEntity() { ENSURE(m_SystemEntity.GetId() == INVALID_ENTITY); m_SystemEntity = AllocateEntityHandle(SYSTEM_ENTITY); m_SimContext.SetSystemEntity(m_SystemEntity); } entity_id_t CComponentManager::AddEntity(const std::wstring& templateName, entity_id_t ent) { ICmpTemplateManager *cmpTemplateManager = static_cast (QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); if (!cmpTemplateManager) { debug_warn(L"No ICmpTemplateManager loaded"); return INVALID_ENTITY; } const CParamNode* tmpl = cmpTemplateManager->LoadTemplate(ent, utf8_from_wstring(templateName)); if (!tmpl) return INVALID_ENTITY; // LoadTemplate will have reported the error // This also ensures that ent does not exist CEntityHandle handle = AllocateEntityHandle(ent); // Construct a component for each child of the root element const CParamNode::ChildrenMap& tmplChilds = tmpl->GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element if (it->first.length() && it->first[0] == '@') continue; CComponentManager::ComponentTypeId cid = LookupCID(it->first); if (cid == CID__Invalid) { LOGERROR("Unrecognised component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } if (!AddComponent(handle, cid, it->second)) { LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? } CMessageCreate msg(ent); PostMessage(ent, msg); return ent; } void CComponentManager::DestroyComponentsSoon(entity_id_t ent) { m_DestructionQueue.push_back(ent); } void CComponentManager::FlushDestroyedComponents() { PROFILE2("Flush Destroyed Components"); while (!m_DestructionQueue.empty()) { // Make a copy of the destruction queue, so that the iterators won't be invalidated if the // CMessageDestroy handlers try to destroy more entities themselves std::vector queue; queue.swap(m_DestructionQueue); for (std::vector::iterator it = queue.begin(); it != queue.end(); ++it) { entity_id_t ent = *it; CEntityHandle handle = LookupEntityHandle(ent); CMessageDestroy msg(ent); PostMessage(ent, msg); // Flatten all the dynamic subscriptions to ensure there are no dangling // references in the 'removed' lists to components we're going to delete // Some components may have dynamically unsubscribed following the Destroy message FlattenDynamicSubscriptions(); // Destroy the components, and remove from m_ComponentsByTypeId: std::map >::iterator iit = m_ComponentsByTypeId.begin(); for (; iit != m_ComponentsByTypeId.end(); ++iit) { std::map::iterator eit = iit->second.find(ent); if (eit != iit->second.end()) { eit->second->Deinit(); RemoveComponentDynamicSubscriptions(eit->second); m_ComponentTypesById[iit->first].dealloc(eit->second); iit->second.erase(ent); handle.GetComponentCache()->interfaces[m_ComponentTypesById[iit->first].iid] = NULL; } } free(handle.GetComponentCache()); m_ComponentCaches.erase(ent); // Remove from m_ComponentsByInterface std::vector >::iterator ifcit = m_ComponentsByInterface.begin(); for (; ifcit != m_ComponentsByInterface.end(); ++ifcit) { ifcit->erase(ent); } } } } IComponent* CComponentManager::QueryInterface(entity_id_t ent, InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return NULL; } boost::unordered_map::const_iterator eit = m_ComponentsByInterface[iid].find(ent); if (eit == m_ComponentsByInterface[iid].end()) { // This entity doesn't implement this interface return NULL; } return eit->second; } CComponentManager::InterfaceList CComponentManager::GetEntitiesWithInterface(InterfaceId iid) const { std::vector > ret; if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return ret; } ret.reserve(m_ComponentsByInterface[iid].size()); boost::unordered_map::const_iterator it = m_ComponentsByInterface[iid].begin(); for (; it != m_ComponentsByInterface[iid].end(); ++it) ret.push_back(*it); std::sort(ret.begin(), ret.end()); // lexicographic pair comparison means this'll sort by entity ID return ret; } static CComponentManager::InterfaceListUnordered g_EmptyEntityMap; const CComponentManager::InterfaceListUnordered& CComponentManager::GetEntitiesWithInterfaceUnordered(InterfaceId iid) const { if ((size_t)iid >= m_ComponentsByInterface.size()) { // Invalid iid return g_EmptyEntityMap; } return m_ComponentsByInterface[iid]; } void CComponentManager::PostMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("Post Message", 0.0005); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // Send the message to components of ent, that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.find(ent); if (eit != emap->second.end()) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(ent, msg); } void CComponentManager::BroadcastMessage(const CMessage& msg) { // Send the message to components of all entities that subscribed locally to this message std::map >::const_iterator it; it = m_LocalMessageSubscriptions.find(msg.GetType()); if (it != m_LocalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, false); } } SendGlobalMessage(INVALID_ENTITY, msg); } void CComponentManager::SendGlobalMessage(entity_id_t ent, const CMessage& msg) { PROFILE2_IFSPIKE("SendGlobalMessage", 0.001); PROFILE2_ATTR("%s", msg.GetScriptHandlerName()); // (Common functionality for PostMessage and BroadcastMessage) // Send the message to components of all entities that subscribed globally to this message std::map >::const_iterator it; it = m_GlobalMessageSubscriptions.find(msg.GetType()); if (it != m_GlobalMessageSubscriptions.end()) { std::vector::const_iterator ctit = it->second.begin(); for (; ctit != it->second.end(); ++ctit) { // Special case: Messages for local entities shouldn't be sent to script // components that subscribed globally, so that we don't have to worry about // them accidentally picking up non-network-synchronised data. if (ENTITY_IS_LOCAL(ent)) { std::map::const_iterator it = m_ComponentTypesById.find(*ctit); if (it != m_ComponentTypesById.end() && it->second.type == CT_Script) continue; } // Find the component instances of this type (if any) std::map >::const_iterator emap = m_ComponentsByTypeId.find(*ctit); if (emap == m_ComponentsByTypeId.end()) continue; // Send the message to all of them std::map::const_iterator eit = emap->second.begin(); for (; eit != emap->second.end(); ++eit) eit->second->HandleMessage(msg, true); } } // Send the message to component instances that dynamically subscribed to this message std::map::iterator dit = m_DynamicMessageSubscriptionsNonsync.find(msg.GetType()); if (dit != m_DynamicMessageSubscriptionsNonsync.end()) { dit->second.Flatten(); const std::vector& dynamic = dit->second.GetComponents(); for (size_t i = 0; i < dynamic.size(); i++) dynamic[i]->HandleMessage(msg, false); } } std::string CComponentManager::GenerateSchema() const { std::string numericOperation = "" "" "" "add" "mul" "" "" ""; std::string schema = "" "" "" + numericOperation + "" "" "0" + numericOperation + "" "" "0" + numericOperation + "" "" "" "" "" "" "" "" "" "" "" "" ""; std::map > interfaceComponentTypes; std::vector componentTypes; for (std::map::const_iterator it = m_ComponentTypesById.begin(); it != m_ComponentTypesById.end(); ++it) { schema += "" "" "" + it->second.schema + "" "" ""; interfaceComponentTypes[it->second.iid].push_back(it->second.name); componentTypes.push_back(it->second.name); } // Declare the implementation of each interface, for documentation for (std::map::const_iterator it = m_InterfaceIdsByName.begin(); it != m_InterfaceIdsByName.end(); ++it) { schema += ""; std::vector& cts = interfaceComponentTypes[it->second]; for (size_t i = 0; i < cts.size(); ++i) schema += ""; schema += ""; } // List all the component types, in alphabetical order (to match the reordering performed by CParamNode). // (We do it this way, rather than ing all the interface definitions (which would additionally perform // a check that we don't use multiple component types of the same interface in one file), because libxml2 gives // useless error messages in the latter case; this way lets it report the real error.) std::sort(componentTypes.begin(), componentTypes.end()); schema += "" "" ""; for (std::vector::const_iterator it = componentTypes.begin(); it != componentTypes.end(); ++it) schema += ""; schema += "" ""; schema += ""; return schema; }