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 26273) +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js (revision 26274) @@ -1,1026 +1,1029 @@ 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, adjusting for tech. "get": function(string) { 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 value = this._template; let args = string.split("/"); for (let arg of args) { value = value[arg]; if (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"); }, "matchLimit": function() { if (!this.get("TrainingRestrictions")) return undefined; return this.get("TrainingRestrictions/MatchLimit"); }, "classes": function() { let template = this.get("Identity"); if (!template) return undefined; return GetIdentityClasses(template); }, "hasClass": function(name) { if (!this._classes) this._classes = this.classes(); return this._classes && this._classes.indexOf(name) != -1; }, "hasClasses": function(array) { if (!this._classes) this._classes = this.classes(); return this._classes && MatchesClassList(this._classes, array); }, "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 {}; 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 0; let ret = 0; for (let type in cost) ret += cost[type]; return ret; }, "techCostMultiplier": function(type) { return +(this.get("Researcher/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() { if (!this.get("Population")) return 0; return +this.get("Population/Bonus"); }, "resistanceStrengths": function() { let resistanceTypes = this.get("Resistance"); if (!resistanceTypes || !resistanceTypes.Entity) return undefined; let resistance = {}; if (resistanceTypes.Entity.Capture) resistance.Capture = +this.get("Resistance/Entity/Capture"); if (resistanceTypes.Entity.Damage) { resistance.Damage = {}; for (let damageType in resistanceTypes.Entity.Damage) resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType); } // ToDo: Resistance to StatusEffects. return resistance; }, "attackTypes": function() { let attack = this.get("Attack"); if (!attack) return undefined; let ret = []; for (let type in 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) { let attackDamageTypes = this.get("Attack/" + type + "/Damage"); if (!attackDamageTypes) return undefined; let damage = {}; for (let damageType in attackDamageTypes) damage[damageType] = +attackDamageTypes[damageType]; return damage; }, "captureStrength": function() { if (!this.get("Attack/Capture")) return undefined; return +this.get("Attack/Capture/Capture") || 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() { let attack = this.get("Attack"); if (!attack) return undefined; let Classes = []; for (let type in 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 the target entity. // TODO: refine using the multiplier "counters": function(target) { let attack = this.get("Attack"); if (!attack) return false; let mcounter = []; for (let type in 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(" ")); } } return target.hasClasses(mcounter); }, // returns, if it exists, the multiplier from each attack against a given class "getMultiplierAgainst": function(type, againstClass) { if (!this.get("Attack/" + type +"")) return undefined; let bonuses = this.get("Attack/" + type + "/Bonuses"); if (bonuses) { for (let b in 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) { const templates = this.get("Trainer/Entities/_string"); if (!templates) return undefined; return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/); }, "researchableTechs": function(gameState, civ) { const templates = this.get("Researcher/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] = 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 }; }, "getResourceType": function() { if (!this.get("ResourceSupply")) return undefined; return this.get("ResourceSupply/Type").split('.')[0]; }, "getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); }, "resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); }, "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+/) : []; }, "isResourceDropsite": function(resourceType) { const types = this.resourceDropsiteTypes(); return types && (!resourceType || types.indexOf(resourceType) !== -1); }, "isTreasure": function() { return this.get("Treasure") !== undefined; }, "treasureResources": function() { if (!this.get("Treasure")) return undefined; let ret = {}; for (let r in this.get("Treasure/Resources")) ret[r] = +this.get("Treasure/Resources/" + r); return ret; }, "garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); }, "garrisonMax": function() { return this.get("GarrisonHolder/Max"); }, "garrisonSize": function() { return this.get("Garrisonable/Size"); }, "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"); }, "isPackable": function() { return this.get("Pack") != undefined; }, "isHuntable": function() { // Do not hunt retaliating animals (dead animals can be used). // Assume entities which can attack, will attack. return this.get("ResourceSupply/KillBeforeGather") && (!this.get("Health") || !this.get("Attack")); }, "walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); }, "trainingCategory": function() { return this.get("TrainingRestrictions/Category"); }, "buildTime": function(researcher) { let time = +this.get("Cost/BuildTime"); if (researcher) time *= researcher.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")) return undefined; let territory = this.get("BuildRestrictions/Territory"); return !territory ? undefined : 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; }, "isTurretHolder": function() { return this.get("TurretHolder") !== 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 || !target.hasClasses(restrictedClasses); }, "isCapturable": function() { return this.get("Capturable") !== undefined; }, "canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; }, "canGarrison": function() { return "Garrisonable" in this._template; }, "canOccupyTurret": function() { return "Turretable" in this._template; }, "isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; }, }); // 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); }, + "queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid) }, + "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() { return this._entity.idle; }, "getStance": function() { return this._entity.stance; }, "unitAIState": function() { return this._entity.unitAIState; }, "unitAIOrderData": function() { return this._entity.unitAIOrderData; }, "hitpoints": function() { return this._entity.hitpoints; }, "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; }, "capturePoints": function() {return this._entity.capturePoints; }, "isInvulnerable": function() { return this._entity.invulnerability || false; }, "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() { return this._entity.trainingQueue; }, "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() { 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() { - return this._entity.resourceSupplyAmount; + return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount(); }, "resourceSupplyNumGatherers": function() { - return this._entity.resourceSupplyNumGatherers; + return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers(); }, "isFull": function() { - if (this._entity.resourceSupplyNumGatherers !== undefined) - return this.maxGatherers() === this._entity.resourceSupplyNumGatherers; + let numGatherers = this.resourceSupplyNumGatherers(); + if (numGatherers) + return this.maxGatherers() === numGatherers; return undefined; }, "resourceCarrying": function() { - return this._entity.resourceCarrying; + return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus(); }, "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") { 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; 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; }, "garrisonHolderID": function() { return this._entity.garrisonHolderID; }, "garrisoned": function() { return this._entity.garrisoned; }, "garrisonedSlots": function() { let count = 0; if (this._entity.garrisoned) for (let ent of this._entity.garrisoned) count += +this._ai._entities.get(ent).garrisonSize(); return count; }, "canGarrisonInside": function() { return this.garrisonedSlots() < this.garrisonMax(); }, /** * returns true if the entity can attack (including capture) the given class. */ "canAttackClass": function(aClass) { let attack = this.get("Attack"); if (!attack) return false; for (let type in attack) { if (type == "Slaughter") continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses)) return true; } return false; }, /** * Derived from Attack.js' similary named function. * @return {boolean} - Whether an entity can attack a given target. */ "canAttackTarget": function(target, allowCapture) { let attackTypes = this.get("Attack"); if (!attackTypes) return false; let canCapture = allowCapture && this.canCapture(target); let health = target.get("Health"); if (!health) return canCapture; for (let type in attackTypes) { if (type == "Capture" ? !canCapture : target.isInvulnerable()) continue; let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); if (!restrictedClasses || !target.hasClasses(restrictedClasses)) return true; } return false; }, "move": function(x, z, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront }); return this; }, "moveToRange": function(x, z, min, max, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront }); return this; }, "attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, // violent, aggressive, defensive, passive, standground "setStance": function(stance) { if (this.getStance() === undefined) return undefined; Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance}); return this; }, "stopMoving": function() { Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": 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, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "occupy-turret": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "attack": function(unitId, allowCapture = true, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront }); return this; }, "collectTreasure": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "collect-treasure", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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, "pushFront": 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, "pushFront": false }); } return this; }, "gather": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); return this; }, "repair": function(target, autocontinue = false, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront }); return this; }, "returnResources": function(target, queued = false, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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, "pushFront": 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, pushFront = false) { 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, "pushFront": pushFront }); 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, "pushFront": false, "metadata": metadata // can be undefined }); return this; }, "research": function(template, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "research", "entity": this.id(), "template": template, "pushFront": pushFront }); 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, pushFront = false) { Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront }); 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/components/AIProxy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 26273) +++ ps/trunk/binaries/data/mods/public/simulation/components/AIProxy.js (revision 26274) @@ -1,407 +1,371 @@ function AIProxy() {} AIProxy.prototype.Schema = ""; /** * AIProxy passes its entity's state data to AI scripts. * * Efficiency is critical: there can be many thousands of entities, * and the data returned by this component is serialized and copied to * the AI thread every turn, so it can be quite expensive. * * We omit all data that can be derived statically from the template XML * files - the AI scripts can parse the templates themselves. * This violates the component interface abstraction and is potentially * fragile if the template formats change (since both the component code * and the AI will have to be updated in sync), but it's not *that* bad * really and it helps performance significantly. * * We also add an optimisation to avoid copying non-changing values. * The first call to GetRepresentation calls GetFullRepresentation, * which constructs the complete entity state representation. * After that, we simply listen to events from the rest of the gameplay code, * and store the changed data in this.changes. * Properties in this.changes will override those previously returned * from GetRepresentation; if a property isn't overridden then the AI scripts * will keep its old value. * * The event handlers should set this.changes.whatever to exactly the * same as GetFullRepresentation would set. */ AIProxy.prototype.Init = function() { this.changes = null; this.needsFullGet = true; this.cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface); }; AIProxy.prototype.Serialize = null; // we have no dynamic state to save AIProxy.prototype.Deserialize = function() { this.Init(); }; AIProxy.prototype.GetRepresentation = function() { // Return the full representation the first time we're called let ret; if (this.needsFullGet) ret = this.GetFullRepresentation(); else ret = this.changes; // Initialise changes to null instead of {}, to avoid memory allocations in the // common case where there will be no changes; event handlers should each reset // it to {} if needed this.changes = null; return ret; }; AIProxy.prototype.NotifyChange = function() { if (this.needsFullGet) { // not yet notified, be sure that the owner is set before doing so // as the Create event is sent only on first ownership changed let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (!cmpOwnership || cmpOwnership.GetOwner() < 0) return false; } if (!this.changes) { this.changes = {}; this.cmpAIInterface.ChangedEntity(this.entity); } return true; }; // AI representation-updating event handlers: AIProxy.prototype.OnPositionChanged = function(msg) { if (!this.NotifyChange()) return; if (msg.inWorld) { this.changes.position = [msg.x, msg.z]; this.changes.angle = msg.a; } else { this.changes.position = undefined; this.changes.angle = undefined; } }; AIProxy.prototype.OnHealthChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.hitpoints = msg.to; }; AIProxy.prototype.OnGarrisonedStateChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.garrisonHolderID = msg.holderID; }; AIProxy.prototype.OnCapturePointsChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.capturePoints = msg.capturePoints; }; AIProxy.prototype.OnInvulnerabilityChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.invulnerability = msg.invulnerability; }; AIProxy.prototype.OnUnitIdleChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.idle = msg.idle; }; AIProxy.prototype.OnUnitStanceChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.stance = msg.to; }; AIProxy.prototype.OnUnitAIStateChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIState = msg.to; }; AIProxy.prototype.OnUnitAIOrderDataChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.unitAIOrderData = msg.to; }; AIProxy.prototype.OnProductionQueueChanged = function(msg) { if (!this.NotifyChange()) return; let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); this.changes.trainingQueue = cmpProductionQueue.GetQueue(); }; AIProxy.prototype.OnGarrisonedUnitsChanged = function(msg) { if (!this.NotifyChange()) return; let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); this.changes.garrisoned = cmpGarrisonHolder.GetEntities(); // Send a message telling a unit garrisoned or ungarrisoned. // I won't check if the unit is still alive so it'll be up to the AI. for (let ent of msg.added) this.cmpAIInterface.PushEvent("Garrison", { "entity": ent, "holder": this.entity }); for (let ent of msg.removed) this.cmpAIInterface.PushEvent("UnGarrison", { "entity": ent, "holder": this.entity }); }; -AIProxy.prototype.OnResourceSupplyChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceSupplyAmount = msg.to; -}; - -AIProxy.prototype.OnResourceSupplyNumGatherersChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceSupplyNumGatherers = msg.to; -}; - -AIProxy.prototype.OnResourceCarryingChanged = function(msg) -{ - if (!this.NotifyChange()) - return; - this.changes.resourceCarrying = msg.to; -}; - AIProxy.prototype.OnFoundationProgressChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationProgress = msg.to; }; AIProxy.prototype.OnFoundationBuildersChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.foundationBuilders = msg.to; }; AIProxy.prototype.OnDropsiteSharingChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.sharedDropsite = msg.shared; }; AIProxy.prototype.OnTerritoryDecayChanged = function(msg) { if (!this.NotifyChange()) return; this.changes.decaying = msg.to; this.cmpAIInterface.PushEvent("TerritoryDecayChanged", msg); }; // TODO: event handlers for all the other things AIProxy.prototype.GetFullRepresentation = function() { this.needsFullGet = false; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); let ret = { // These properties are constant and won't need to be updated "id": this.entity, "template": cmpTemplateManager.GetCurrentTemplateName(this.entity) }; let cmpPosition = Engine.QueryInterface(this.entity, IID_Position); if (cmpPosition) { // Updated by OnPositionChanged if (cmpPosition.IsInWorld()) { let pos = cmpPosition.GetPosition2D(); ret.position = [pos.x, pos.y]; ret.angle = cmpPosition.GetRotation().y; } else { ret.position = undefined; ret.angle = undefined; } } let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (cmpHealth) { // Updated by OnHealthChanged ret.hitpoints = cmpHealth.GetHitpoints(); } let cmpResistance = Engine.QueryInterface(this.entity, IID_Resistance); if (cmpResistance) ret.invulnerability = cmpResistance.IsInvulnerable(); let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership) { // Updated by OnOwnershipChanged ret.owner = cmpOwnership.GetOwner(); } let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); if (cmpUnitAI) { // Updated by OnUnitIdleChanged ret.idle = cmpUnitAI.IsIdle(); // Updated by OnUnitStanceChanged ret.stance = cmpUnitAI.GetStanceName(); // Updated by OnUnitAIStateChanged ret.unitAIState = cmpUnitAI.GetCurrentState(); // Updated by OnUnitAIOrderDataChanged ret.unitAIOrderData = cmpUnitAI.GetOrderData(); } let cmpProductionQueue = Engine.QueryInterface(this.entity, IID_ProductionQueue); if (cmpProductionQueue) { // Updated by OnProductionQueueChanged ret.trainingQueue = cmpProductionQueue.GetQueue(); } let cmpFoundation = Engine.QueryInterface(this.entity, IID_Foundation); if (cmpFoundation) { // Updated by OnFoundationProgressChanged ret.foundationProgress = cmpFoundation.GetBuildPercentage(); } - let cmpResourceSupply = Engine.QueryInterface(this.entity, IID_ResourceSupply); - if (cmpResourceSupply) - { - // Updated by OnResourceSupplyChanged - ret.resourceSupplyAmount = cmpResourceSupply.GetCurrentAmount(); - ret.resourceSupplyNumGatherers = cmpResourceSupply.GetNumGatherers(); - } - - let cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); - if (cmpResourceGatherer) - { - // Updated by OnResourceCarryingChanged - ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); - } - let cmpResourceDropsite = Engine.QueryInterface(this.entity, IID_ResourceDropsite); if (cmpResourceDropsite) { // Updated by OnDropsiteSharingChanged ret.sharedDropsite = cmpResourceDropsite.IsShared(); } let cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder); if (cmpGarrisonHolder) { // Updated by OnGarrisonedUnitsChanged ret.garrisoned = cmpGarrisonHolder.GetEntities(); } let cmpGarrisonable = Engine.QueryInterface(this.entity, IID_Garrisonable); if (cmpGarrisonable) { // Updated by OnGarrisonedStateChanged ret.garrisonHolderID = cmpGarrisonable.HolderID(); } let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay); if (cmpTerritoryDecay) ret.decaying = cmpTerritoryDecay.IsDecaying(); let cmpCapturable = Engine.QueryInterface(this.entity, IID_Capturable); if (cmpCapturable) ret.capturePoints = cmpCapturable.GetCapturePoints(); return ret; }; // AI event handlers: // (These are passed directly as events to the AI scripts, rather than updating // our proxy representation.) // (This shouldn't include extremely high-frequency events, like PositionChanged, // because that would be very expensive and AI will rarely care about all those // events.) // special case: this changes the state and sends an event. AIProxy.prototype.OnOwnershipChanged = function(msg) { this.NotifyChange(); if (msg.from == INVALID_PLAYER) { this.cmpAIInterface.PushEvent("Create", { "entity": msg.entity }); return; } if (msg.to == INVALID_PLAYER) { this.cmpAIInterface.PushEvent("Destroy", { "entity": msg.entity }); return; } this.changes.owner = msg.to; this.cmpAIInterface.PushEvent("OwnershipChanged", msg); }; AIProxy.prototype.OnAttacked = function(msg) { this.cmpAIInterface.PushEvent("Attacked", msg); }; AIProxy.prototype.OnConstructionFinished = function(msg) { this.cmpAIInterface.PushEvent("ConstructionFinished", msg); }; AIProxy.prototype.OnTrainingStarted = function(msg) { this.cmpAIInterface.PushEvent("TrainingStarted", msg); }; AIProxy.prototype.OnTrainingFinished = function(msg) { this.cmpAIInterface.PushEvent("TrainingFinished", msg); }; AIProxy.prototype.OnAIMetadata = function(msg) { this.cmpAIInterface.PushEvent("AIMetadata", msg); }; Engine.RegisterComponentType(IID_AIProxy, "AIProxy", AIProxy); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 26273) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceGatherer.js (revision 26274) @@ -1,532 +1,521 @@ function ResourceGatherer() {} ResourceGatherer.prototype.Schema = "Lets the unit gather resources from entities that have the ResourceSupply component." + "" + "2.0" + "1.0" + "" + "1" + "3" + "3" + "2" + "" + "" + "10" + "10" + "10" + "10" + "" + "" + "" + "" + "" + "" + "" + "" + "" + Resources.BuildSchema("positiveDecimal", [], true) + "" + "" + Resources.BuildSchema("positiveDecimal") + ""; /* * Call interval will be determined by gather rate, * so always gather integer amount. */ ResourceGatherer.prototype.GATHER_AMOUNT = 1; ResourceGatherer.prototype.Init = function() { this.capacities = {}; this.carrying = {}; // { generic type: integer amount currently carried } // (Note that this component supports carrying multiple types of resources, // each with an independent capacity, but the rest of the game currently // ensures and assumes we'll only be carrying one type at once) // The last exact type gathered, so we can render appropriate props this.lastCarriedType = undefined; // { generic, specific } }; /** * Returns data about what resources the unit is currently carrying, * in the form [ {"type":"wood", "amount":7, "max":10} ] */ ResourceGatherer.prototype.GetCarryingStatus = function() { let ret = []; for (let type in this.carrying) { ret.push({ "type": type, "amount": this.carrying[type], "max": +this.GetCapacity(type) }); } return ret; }; /** * Used to instantly give resources to unit * @param resources The same structure as returned form GetCarryingStatus */ ResourceGatherer.prototype.GiveResources = function(resources) { for (let resource of resources) this.carrying[resource.type] = +resource.amount; - - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * Returns the generic type of one particular resource this unit is * currently carrying, or undefined if none. */ ResourceGatherer.prototype.GetMainCarryingType = function() { // Return the first key, if any for (let type in this.carrying) return type; return undefined; }; /** * Returns the exact resource type we last picked up, as long as * we're still carrying something similar enough, in the form * { generic, specific } */ ResourceGatherer.prototype.GetLastCarriedType = function() { if (this.lastCarriedType && this.lastCarriedType.generic in this.carrying) return this.lastCarriedType; return undefined; }; ResourceGatherer.prototype.SetLastCarriedType = function(lastCarriedType) { this.lastCarriedType = lastCarriedType; }; // Since this code is very performancecritical and applying technologies quite slow, cache it. ResourceGatherer.prototype.RecalculateGatherRates = function() { this.baseSpeed = ApplyValueModificationsToEntity("ResourceGatherer/BaseSpeed", +this.template.BaseSpeed, this.entity); this.rates = {}; for (let r in this.template.Rates) { let type = r.split("."); if (!Resources.GetResource(type[0]).subtypes[type[1]]) { error("Resource subtype not found: " + type[0] + "." + type[1]); continue; } let rate = ApplyValueModificationsToEntity("ResourceGatherer/Rates/" + r, +this.template.Rates[r], this.entity); this.rates[r] = rate * this.baseSpeed; } }; ResourceGatherer.prototype.RecalculateCapacities = function() { this.capacities = {}; for (let r in this.template.Capacities) this.capacities[r] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + r, +this.template.Capacities[r], this.entity); }; ResourceGatherer.prototype.RecalculateCapacity = function(type) { if (type in this.capacities) this.capacities[type] = ApplyValueModificationsToEntity("ResourceGatherer/Capacities/" + type, +this.template.Capacities[type], this.entity); }; ResourceGatherer.prototype.GetGatherRates = function() { return this.rates; }; ResourceGatherer.prototype.GetGatherRate = function(resourceType) { if (!this.template.Rates[resourceType]) return 0; return this.rates[resourceType]; }; ResourceGatherer.prototype.GetCapacity = function(resourceType) { if (!this.template.Capacities[resourceType]) return 0; return this.capacities[resourceType]; }; ResourceGatherer.prototype.GetRange = function() { return { "max": +this.template.MaxDistance, "min": 0 }; }; /** * @param {number} target - The target to gather from. * @param {number} callerIID - The IID to notify on specific events. * @return {boolean} - Whether we started gathering. */ ResourceGatherer.prototype.StartGathering = function(target, callerIID) { if (this.target) this.StopGathering(); let rate = this.GetTargetGatherRate(target); if (!rate) return false; let cmpResourceSupply = Engine.QueryInterface(target, IID_ResourceSupply); if (!cmpResourceSupply || !cmpResourceSupply.AddActiveGatherer(this.entity)) return false; let resourceType = cmpResourceSupply.GetType(); // If we've already got some resources but they're the wrong type, // drop them first to ensure we're only ever carrying one type. if (this.IsCarryingAnythingExcept(resourceType.generic)) this.DropResources(); let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("gather_" + resourceType.specific, false, 1.0); // Calculate timing based on gather rates. // This allows the gather rate to control how often we gather, instead of how much. let timing = 1000 / rate; this.target = target; this.callerIID = callerIID; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); this.timer = cmpTimer.SetInterval(this.entity, IID_ResourceGatherer, "PerformGather", timing, timing, null); return true; }; /** * @param {string} reason - The reason why we stopped gathering used to notify the caller. */ ResourceGatherer.prototype.StopGathering = function(reason) { if (!this.target) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timer); delete this.timer; let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); if (cmpResourceSupply) cmpResourceSupply.RemoveGatherer(this.entity); delete this.target; let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); if (cmpVisual) cmpVisual.SelectAnimation("idle", false, 1.0); // The callerIID component may start again, // replacing the callerIID, hence save that. let callerIID = this.callerIID; delete this.callerIID; if (reason && callerIID) { let component = Engine.QueryInterface(this.entity, callerIID); if (component) component.ProcessMessage(reason, null); } }; /** * Gather from our target entity. * @params - data and lateness are unused. */ ResourceGatherer.prototype.PerformGather = function(data, lateness) { let cmpResourceSupply = Engine.QueryInterface(this.target, IID_ResourceSupply); if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0) { this.StopGathering("TargetInvalidated"); return; } if (!this.IsTargetInRange(this.target)) { this.StopGathering("OutOfRange"); return; } // ToDo: Enable entities to keep facing a target. Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target); let type = cmpResourceSupply.GetType(); if (!this.carrying[type.generic]) this.carrying[type.generic] = 0; let maxGathered = this.GetCapacity(type.generic) - this.carrying[type.generic]; let status = cmpResourceSupply.TakeResources(Math.min(this.GATHER_AMOUNT, maxGathered)); this.carrying[type.generic] += status.amount; this.lastCarriedType = type; // Update stats of how much the player collected. // (We have to do it here rather than at the dropsite, because we // need to know what subtype it was.) let cmpStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker); if (cmpStatisticsTracker) cmpStatisticsTracker.IncreaseResourceGatheredCounter(type.generic, status.amount, type.specific); - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); - if (!this.CanCarryMore(type.generic)) this.StopGathering("InventoryFilled"); else if (status.exhausted) this.StopGathering("TargetInvalidated"); }; /** * Compute the amount of resources collected per second from the target. * Returns 0 if resources cannot be collected (e.g. the target doesn't * exist, or is the wrong type). */ ResourceGatherer.prototype.GetTargetGatherRate = function(target) { let cmpResourceSupply = QueryMiragedInterface(target, IID_ResourceSupply); if (!cmpResourceSupply || cmpResourceSupply.GetCurrentAmount() <= 0) return 0; let type = cmpResourceSupply.GetType(); let rate = 0; if (type.specific) rate = this.GetGatherRate(type.generic + "." + type.specific); if (rate == 0 && type.generic) rate = this.GetGatherRate(type.generic); let diminishingReturns = cmpResourceSupply.GetDiminishingReturns(); if (diminishingReturns) rate *= diminishingReturns; return rate; }; /** * @param {number} target - The entity ID of the target to check. * @return {boolean} - Whether we can gather from the target. */ ResourceGatherer.prototype.CanGather = function(target) { return this.GetTargetGatherRate(target) > 0; }; /** * Returns whether this unit can carry more of the given type of resource. * (This ignores whether the unit is actually able to gather that * resource type or not.) */ ResourceGatherer.prototype.CanCarryMore = function(type) { let amount = this.carrying[type] || 0; return amount < this.GetCapacity(type); }; ResourceGatherer.prototype.IsCarrying = function(type) { let amount = this.carrying[type] || 0; return amount > 0; }; /** * Returns whether this unit is carrying any resources of a type that is * not the requested type. (This is to support cases where the unit is * only meant to be able to carry one type at once.) */ ResourceGatherer.prototype.IsCarryingAnythingExcept = function(exceptedType) { for (let type in this.carrying) if (type != exceptedType) return true; return false; }; /** * @param {number} target - The entity to check. * @param {boolean} checkCarriedResource - Whether we need to check the resource we are carrying. * @return {boolean} - Whether we can return carried resources. */ ResourceGatherer.prototype.CanReturnResource = function(target, checkCarriedResource) { let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return false; if (checkCarriedResource) { let type = this.GetMainCarryingType(); if (!type || !cmpResourceDropsite.AcceptsType(type)) return false; } let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); if (cmpOwnership && IsOwnedByPlayer(cmpOwnership.GetOwner(), target)) return true; let cmpPlayer = QueryOwnerInterface(this.entity); return cmpPlayer && cmpPlayer.HasSharedDropsites() && cmpResourceDropsite.IsShared() && cmpOwnership && IsOwnedByMutualAllyOfPlayer(cmpOwnership.GetOwner(), target); }; /** * Transfer our carried resources to our owner immediately. * Only resources of the appropriate types will be transferred. * (This should typically be called after reaching a dropsite.) * * @param {number} target - The target entity ID to drop resources at. */ ResourceGatherer.prototype.CommitResources = function(target) { let cmpResourceDropsite = Engine.QueryInterface(target, IID_ResourceDropsite); if (!cmpResourceDropsite) return; let change = cmpResourceDropsite.ReceiveResources(this.carrying, this.entity); - let changed = false; for (let type in change) { this.carrying[type] -= change[type]; if (this.carrying[type] == 0) delete this.carrying[type]; - changed = true; } - - if (changed) - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * Drop all currently-carried resources. * (Currently they just vanish after being dropped - we don't bother depositing * them onto the ground.) */ ResourceGatherer.prototype.DropResources = function() { this.carrying = {}; - - Engine.PostMessage(this.entity, MT_ResourceCarryingChanged, { "to": this.GetCarryingStatus() }); }; /** * @return {string} - A generic resource type if we were tasked to gather. */ ResourceGatherer.prototype.GetTaskedResourceType = function() { return this.taskedResourceType; }; /** * @param {string} type - A generic resource type. */ ResourceGatherer.prototype.AddToPlayerCounter = function(type) { // We need to be removed from the player counter first. if (this.taskedResourceType) return; let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.AddResourceGatherer(type); this.taskedResourceType = type; }; /** * @param {number} playerid - Optionally a player ID. */ ResourceGatherer.prototype.RemoveFromPlayerCounter = function(playerid) { if (!this.taskedResourceType) return; let cmpPlayer = playerid != undefined ? QueryPlayerIDInterface(playerid) : QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer) cmpPlayer.RemoveResourceGatherer(this.taskedResourceType); delete this.taskedResourceType; }; /** * @param {number} - The entity ID of the target to check. * @return {boolean} - Whether this entity is in range of its target. */ ResourceGatherer.prototype.IsTargetInRange = function(target) { return Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager). IsInTargetRange(this.entity, target, 0, +this.template.MaxDistance, false); }; // Since we cache gather rates, we need to make sure we update them when tech changes. // and when our owner change because owners can had different techs. ResourceGatherer.prototype.OnValueModification = function(msg) { if (msg.component != "ResourceGatherer") return; // NB: at the moment, 0 A.D. always uses the fast path, the other is mod support. if (msg.valueNames.length === 1) { if (msg.valueNames[0].indexOf("Capacities") !== -1) this.RecalculateCapacity(msg.valueNames[0].substr(28)); else this.RecalculateGatherRates(); } else { this.RecalculateGatherRates(); this.RecalculateCapacities(); } }; ResourceGatherer.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) { this.RemoveFromPlayerCounter(msg.from); return; } if (this.lastGathered && msg.from !== INVALID_PLAYER) { const resource = this.taskedResourceType; this.RemoveFromPlayerCounter(msg.from); this.AddToPlayerCounter(resource); } this.RecalculateGatherRates(); this.RecalculateCapacities(); }; ResourceGatherer.prototype.OnGlobalInitGame = function(msg) { this.RecalculateGatherRates(); this.RecalculateCapacities(); }; ResourceGatherer.prototype.OnMultiplierChanged = function(msg) { let cmpPlayer = QueryOwnerInterface(this.entity, IID_Player); if (cmpPlayer && msg.player == cmpPlayer.GetPlayerID()) this.RecalculateGatherRates(); }; Engine.RegisterComponentType(IID_ResourceGatherer, "ResourceGatherer", ResourceGatherer); Index: ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 26273) +++ ps/trunk/binaries/data/mods/public/simulation/components/ResourceSupply.js (revision 26274) @@ -1,507 +1,503 @@ function ResourceSupply() {} ResourceSupply.prototype.Schema = "Provides a supply of one particular type of resource." + "" + "1000" + "1000" + "food.meat" + "false" + "25" + "0.8" + "" + "" + "2" + "1000" + "" + "" + "alive" + "2" + "1000" + "500" + "" + "" + "dead notGathered" + "-2" + "1000" + "" + "" + "dead" + "-1" + "1000" + "500" + "" + "" + "" + "" + "" + "" + "" + "Infinity" + "" + "" + "" + "Infinity" + "" + "" + "" + Resources.BuildChoicesSchema(true) + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "alive" + "dead" + "gathered" + "notGathered" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; ResourceSupply.prototype.Init = function() { this.amount = +(this.template.Initial || this.template.Max); // Includes the ones that are tasked but not here yet, i.e. approaching. this.gatherers = []; this.activeGatherers = []; let [type, subtype] = this.template.Type.split('.'); this.cachedType = { "generic": type, "specific": subtype }; if (this.template.Change) { this.timers = {}; this.cachedChanges = {}; } }; ResourceSupply.prototype.IsInfinite = function() { return !isFinite(+this.template.Max); }; ResourceSupply.prototype.GetKillBeforeGather = function() { return this.template.KillBeforeGather == "true"; }; ResourceSupply.prototype.GetMaxAmount = function() { return this.maxAmount; }; ResourceSupply.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupply.prototype.GetMaxGatherers = function() { return +this.template.MaxGatherers; }; ResourceSupply.prototype.GetNumGatherers = function() { return this.gatherers.length; }; /** * @return {number} - The number of currently active gatherers. */ ResourceSupply.prototype.GetNumActiveGatherers = function() { return this.activeGatherers.length; }; /** * @return {{ "generic": string, "specific": string }} An object containing the subtype and the generic type. All resources must have both. */ ResourceSupply.prototype.GetType = function() { return this.cachedType; }; /** * @param {number} gathererID - The gatherer's entity id. * @return {boolean} - Whether the ResourceSupply can have this additional gatherer or it is already gathering. */ ResourceSupply.prototype.IsAvailableTo = function(gathererID) { return this.IsAvailable() || this.IsGatheringUs(gathererID); }; /** * @return {boolean} - Whether this entity can have an additional gatherer. */ ResourceSupply.prototype.IsAvailable = function() { return this.amount && this.gatherers.length < this.GetMaxGatherers(); }; /** * @param {number} entity - The entityID to check for. * @return {boolean} - Whether the given entity is already gathering at us. */ ResourceSupply.prototype.IsGatheringUs = function(entity) { return this.gatherers.indexOf(entity) !== -1; }; /** * Each additional gatherer decreases the rate following a geometric sequence, with diminishingReturns as ratio. * @return {number} The diminishing return if any, null otherwise. */ ResourceSupply.prototype.GetDiminishingReturns = function() { if (!this.template.DiminishingReturns) return null; let diminishingReturns = ApplyValueModificationsToEntity("ResourceSupply/DiminishingReturns", +this.template.DiminishingReturns, this.entity); if (!diminishingReturns) return null; let numGatherers = this.GetNumGatherers(); if (numGatherers > 1) return diminishingReturns == 1 ? 1 : (1 - Math.pow(diminishingReturns, numGatherers)) / (1 - diminishingReturns) / numGatherers; return null; }; /** * @param {number} amount The amount of resources that should be taken from the resource supply. The amount must be positive. * @return {{ "amount": number, "exhausted": boolean }} The current resource amount in the entity and whether it's exhausted or not. */ ResourceSupply.prototype.TakeResources = function(amount) { if (this.IsInfinite()) return { "amount": amount, "exhausted": false }; return { "amount": Math.abs(this.Change(-amount)), "exhausted": this.amount == 0 }; }; /** * @param {number} change - The amount to change the resources with (can be negative). * @return {number} - The actual change in resourceSupply. */ ResourceSupply.prototype.Change = function(change) { // Before changing the amount, activate Fogging if necessary to hide changes let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging); if (cmpFogging) cmpFogging.Activate(); let oldAmount = this.amount; this.amount = Math.min(Math.max(oldAmount + change, 0), this.maxAmount); // Remove entities that have been exhausted. if (this.amount == 0) Engine.DestroyEntity(this.entity); let actualChange = this.amount - oldAmount; if (actualChange != 0) { Engine.PostMessage(this.entity, MT_ResourceSupplyChanged, { "from": oldAmount, "to": this.amount }); this.CheckTimers(); } return actualChange; }; /** * @param {number} newValue - The value to set the current amount to. */ ResourceSupply.prototype.SetAmount = function(newValue) { // We currently don't support changing to or fro Infinity. if (this.IsInfinite() || newValue === Infinity) return; this.Change(newValue - this.amount); }; /** * @param {number} gathererID - The gatherer to add. * @return {boolean} - Whether the gatherer was successfully added to the entity's gatherers list * or the entity was already gathering us. */ ResourceSupply.prototype.AddGatherer = function(gathererID) { if (!this.IsAvailable()) return false; if (this.IsGatheringUs(gathererID)) return true; this.gatherers.push(gathererID); - Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); return true; }; /** * @param {number} player - The playerID owning the gatherer. * @param {number} entity - The entityID gathering. * * @return {boolean} - Whether the gatherer was successfully added to the active-gatherers list * or the entity was already in that list. */ ResourceSupply.prototype.AddActiveGatherer = function(entity) { if (!this.AddGatherer(entity)) return false; if (this.activeGatherers.indexOf(entity) == -1) { this.activeGatherers.push(entity); this.CheckTimers(); } return true; }; /** * @param {number} gathererID - The gatherer's entity id. * @todo: Should this return false if the gatherer didn't gather from said resource? */ ResourceSupply.prototype.RemoveGatherer = function(gathererID) { let index = this.gatherers.indexOf(gathererID); if (index != -1) - { this.gatherers.splice(index, 1); - Engine.PostMessage(this.entity, MT_ResourceSupplyNumGatherersChanged, { "to": this.GetNumGatherers() }); - } index = this.activeGatherers.indexOf(gathererID); if (index == -1) return; this.activeGatherers.splice(index, 1); this.CheckTimers(); }; /** * Checks whether a timer ought to be added or removed. */ ResourceSupply.prototype.CheckTimers = function() { if (!this.template.Change || this.IsInfinite()) return; for (let changeKey in this.template.Change) { if (!this.CheckState(changeKey)) { this.StopTimer(changeKey); continue; } let template = this.template.Change[changeKey]; if (this.amount < +(template.LowerLimit || -1) || this.amount > +(template.UpperLimit || this.GetMaxAmount())) { this.StopTimer(changeKey); continue; } if (this.cachedChanges[changeKey] == 0) { this.StopTimer(changeKey); continue; } if (!this.timers[changeKey]) this.StartTimer(changeKey); } }; /** * This verifies whether the current state of the supply matches the ones needed * for the specific timer to run. * * @param {string} changeKey - The name of the Change to verify the state for. * @return {boolean} - Whether the timer may run. */ ResourceSupply.prototype.CheckState = function(changeKey) { let template = this.template.Change[changeKey]; if (!template.State) return true; let states = template.State; let cmpHealth = Engine.QueryInterface(this.entity, IID_Health); if (states.indexOf("alive") != -1 && !cmpHealth && states.indexOf("dead") == -1 || states.indexOf("dead") != -1 && cmpHealth && states.indexOf("alive") == -1) return false; let activeGatherers = this.GetNumActiveGatherers(); if (states.indexOf("gathered") != -1 && activeGatherers == 0 && states.indexOf("notGathered") == -1 || states.indexOf("notGathered") != -1 && activeGatherers > 0 && states.indexOf("gathered") == -1) return false; return true; }; /** * @param {string} changeKey - The name of the Change to apply to the entity. */ ResourceSupply.prototype.StartTimer = function(changeKey) { if (this.timers[changeKey]) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); let interval = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Interval", +(this.template.Change[changeKey].Interval || 1000), this.entity); this.timers[changeKey] = cmpTimer.SetInterval(this.entity, IID_ResourceSupply, "TimerTick", interval, interval, changeKey); }; /** * @param {string} changeKey - The name of the change to stop the timer for. */ ResourceSupply.prototype.StopTimer = function(changeKey) { if (!this.timers[changeKey]) return; let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.timers[changeKey]); delete this.timers[changeKey]; }; /** * @param {string} changeKey - The name of the change to apply to the entity. */ ResourceSupply.prototype.TimerTick = function(changeKey) { let template = this.template.Change[changeKey]; if (!template || !this.Change(this.cachedChanges[changeKey])) this.StopTimer(changeKey); }; /** * Since the supposed changes can be affected by modifications, and applying those * are slow, do not calculate them every timer tick. */ ResourceSupply.prototype.RecalculateValues = function() { this.maxAmount = ApplyValueModificationsToEntity("ResourceSupply/Max", +this.template.Max, this.entity); if (!this.template.Change || this.IsInfinite()) return; for (let changeKey in this.template.Change) this.cachedChanges[changeKey] = ApplyValueModificationsToEntity("ResourceSupply/Change/" + changeKey + "/Value", +this.template.Change[changeKey].Value, this.entity); this.CheckTimers(); }; /** * @param {{ "component": string, "valueNames": string[] }} msg - Message containing a list of values that were changed. */ ResourceSupply.prototype.OnValueModification = function(msg) { if (msg.component != "ResourceSupply") return; this.RecalculateValues(); }; /** * @param {{ "from": number, "to": number }} msg - Message containing the old new owner. */ ResourceSupply.prototype.OnOwnershipChanged = function(msg) { if (msg.to == INVALID_PLAYER) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); for (let changeKey in this.timers) cmpTimer.CancelTimer(this.timers[changeKey]); } else this.RecalculateValues(); }; /** * @param {{ "entity": number, "newentity": number }} msg - Message to what the entity has been renamed. */ ResourceSupply.prototype.OnEntityRenamed = function(msg) { let cmpResourceSupplyNew = Engine.QueryInterface(msg.newentity, IID_ResourceSupply); if (cmpResourceSupplyNew) cmpResourceSupplyNew.SetAmount(this.GetCurrentAmount()); }; function ResourceSupplyMirage() {} ResourceSupplyMirage.prototype.Init = function(cmpResourceSupply) { this.maxAmount = cmpResourceSupply.GetMaxAmount(); this.amount = cmpResourceSupply.GetCurrentAmount(); this.type = cmpResourceSupply.GetType(); this.isInfinite = cmpResourceSupply.IsInfinite(); this.killBeforeGather = cmpResourceSupply.GetKillBeforeGather(); this.maxGatherers = cmpResourceSupply.GetMaxGatherers(); this.numGatherers = cmpResourceSupply.GetNumGatherers(); }; ResourceSupplyMirage.prototype.GetMaxAmount = function() { return this.maxAmount; }; ResourceSupplyMirage.prototype.GetCurrentAmount = function() { return this.amount; }; ResourceSupplyMirage.prototype.GetType = function() { return this.type; }; ResourceSupplyMirage.prototype.IsInfinite = function() { return this.isInfinite; }; ResourceSupplyMirage.prototype.GetKillBeforeGather = function() { return this.killBeforeGather; }; ResourceSupplyMirage.prototype.GetMaxGatherers = function() { return this.maxGatherers; }; ResourceSupplyMirage.prototype.GetNumGatherers = function() { return this.numGatherers; }; // Apply diminishing returns with more gatherers, for e.g. infinite farms. For most resources this has no effect // (GetDiminishingReturns will return null). We can assume that for resources that are miraged this is the case. ResourceSupplyMirage.prototype.GetDiminishingReturns = function() { return null; }; Engine.RegisterGlobal("ResourceSupplyMirage", ResourceSupplyMirage); ResourceSupply.prototype.Mirage = function() { let mirage = new ResourceSupplyMirage(); mirage.Init(this); return mirage; }; Engine.RegisterComponentType(IID_ResourceSupply, "ResourceSupply", ResourceSupply); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js (revision 26273) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceGatherer.js (revision 26274) @@ -1,7 +1 @@ Engine.RegisterInterface("ResourceGatherer"); - -/** - * Message of the form { "to": [{ "type": string, "amount": number, "max": number }] } - * sent from ResourceGatherer component whenever the amount of carried resources changes. - */ -Engine.RegisterMessageType("ResourceCarryingChanged"); Index: ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (revision 26273) +++ ps/trunk/binaries/data/mods/public/simulation/components/interfaces/ResourceSupply.js (revision 26274) @@ -1,13 +1,7 @@ Engine.RegisterInterface("ResourceSupply"); /** * Message of the form { "from": number, "to": number } * sent from ResourceSupply component whenever the supply level changes. */ Engine.RegisterMessageType("ResourceSupplyChanged"); - -/** - * Message of the form { "to": number } - * sent from ResourceSupply component whenever the number of gatherer changes. - */ -Engine.RegisterMessageType("ResourceSupplyNumGatherersChanged"); Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 26273) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 26274) @@ -1,706 +1,731 @@ /* Copyright (C) 2022 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 "FunctionWrapper.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "StructuredClone.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { - ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context); + ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment); ~ScriptInterface_impl(); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. std::shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; /** * Constructor for ScriptRequest - here because it needs access into ScriptInterface_impl. */ ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx), glob(scriptInterface.m->m_glob), nativeScope(scriptInterface.m->m_nativeScope), m_ScriptInterface(scriptInterface) { m_FormerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_FormerRealm); } ScriptRequest::ScriptRequest(JSContext* cx) : ScriptRequest(ScriptInterface::CmptPrivate::GetScriptInterface(cx)) { } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } const ScriptInterface& ScriptRequest::GetScriptInterface() const { return m_ScriptInterface; } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(cx); for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!Script::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(cx); std::wstring str; if (!Script::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } JS::Value deepcopy(const ScriptRequest& rq, JS::HandleValue val) { if (val.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy requires one argument."); return JS::UndefinedValue(); } JS::RootedValue ret(rq.cx, Script::DeepCopy(rq, val)); if (ret.isNullOrUndefined()) { ScriptException::Raise(rq, "deepcopy StructureClone copy failed."); return JS::UndefinedValue(); } return ret; } JS::Value deepfreeze(const ScriptInterface& scriptInterface, JS::HandleValue val) { ScriptRequest rq(scriptInterface); if (!val.isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return JS::UndefinedValue(); } Script::FreezeObject(rq, val, true); return val; } void ProfileStart(const std::string& regionName) { const char* name = "(ProfileStart)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!regionName.empty()) name = StringFlyweight(regionName).get().c_str(); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); } void ProfileStop() { if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); } void ProfileAttribute(const std::string& attr) { const char* name = "(ProfileAttribute)"; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; if (!attr.empty()) name = StringFlyweight(attr).get().c_str(); g_Profiler2.RecordAttribute("%s", name); } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) const { if (m->m_rng == nullptr) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } bool ScriptInterface::Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::CmptPrivate::GetScriptInterface(cx).MathRandom(r)) return false; args.rval().setNumber(r); return true; } -ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context) : +ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const std::shared_ptr& context, JS::Compartment* compartment) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); + + if (compartment) + creationOpt.setExistingCompartment(compartment); + else + // This is the default behaviour. + creationOpt.setNewCompartmentAndZone(); + JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); // These first 4 actually use CallArgs & thus don't use ScriptFunction JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register(m_cx, m_glob, "clone"); ScriptFunction::Register(m_cx, m_glob, "deepfreeze"); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); ScriptFunction::Register<&ProfileStart>(m_cx, m_nativeScope, "ProfileStart"); ScriptFunction::Register<&ProfileStop>(m_cx, m_nativeScope, "ProfileStop"); ScriptFunction::Register<&ProfileAttribute>(m_cx, m_nativeScope, "ProfileAttribute"); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context) : - m(std::make_unique(nativeScopeName, context)) + m(std::make_unique(nativeScopeName, context, nullptr)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); } +ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor) +{ + ScriptRequest nrq(neighbor); + JS::Compartment* comp = JS::GetCompartmentForRealm(JS::GetCurrentRealmOrNull(nrq.cx)); + m = std::make_unique(nativeScopeName, neighbor.GetContext(), comp); + + // Profiler stats table isn't thread-safe, so only enable this on the main thread + if (Threading::IsMainThread()) + { + if (g_ScriptStatsTable) + g_ScriptStatsTable->Add(this, debugName); + } + + ScriptRequest rq(this); + m_CmptPrivate.pScriptInterface = this; + JS::SetRealmPrivate(JS::GetObjectRealmOrNull(rq.glob), (void*)&m_CmptPrivate); +} + ScriptInterface::~ScriptInterface() { if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } const ScriptInterface& ScriptInterface::CmptPrivate::GetScriptInterface(JSContext *cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); ENSURE(pCmptPrivate); return *pCmptPrivate->pScriptInterface; } void* ScriptInterface::CmptPrivate::GetCBData(JSContext *cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS::GetRealmPrivate(JS::GetCurrentRealmOrNull(cx)); return pCmptPrivate ? pCmptPrivate->pCBData : nullptr; } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq) { return ScriptInterface::CmptPrivate::GetCBData(rq.cx); } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } std::shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject objOut(rq.cx); if (!JS::Construct(rq.cx, ctor, argv, &objOut)) out.setNull(); else out.setObjectOrNull(objOut); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 26273) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 26274) @@ -1,292 +1,302 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "ps/Errors.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptRequest.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local std::shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } class Path; using VfsPath = Path; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; public: /** * Constructor. * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const std::shared_ptr& context); + /** + * Alternate constructor. This creates the new Realm in the same Compartment as the neighbor scriptInterface. + * This means that data can be freely exchanged between these two script interfaces without cloning. + * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will + * be placed into, as a scoping mechanism; typically "Engine" + * @param debugName Name of this interface for CScriptStats purposes. + * @param scriptInterface 'Neighbor' scriptInterface to share a compartment with. + */ + ScriptInterface(const char* nativeScopeName, const char* debugName, const ScriptInterface& neighbor); + ~ScriptInterface(); struct CmptPrivate { friend class ScriptInterface; public: static const ScriptInterface& GetScriptInterface(JSContext* cx); static void* GetCBData(JSContext* cx); protected: ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions }; void SetCallbackData(void* pCBData); /** * Convert the CmptPrivate callback data to T* */ template static T* ObjectFromCBData(const ScriptRequest& rq) { static_assert(!std::is_same_v); ScriptInterface::CmptPrivate::GetCBData(rq.cx); return static_cast(ObjectFromCBData(rq)); } /** * Variant for the function wrapper. */ template static T* ObjectFromCBData(const ScriptRequest& rq, JS::CallArgs&) { return ObjectFromCBData(rq); } /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; std::shared_ptr GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number generator with a seeded, network-synced one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Get an object from the global scope or any lexical scope. * This can return globally accessible objects even if they are not properties * of the global object (e.g. ES6 class definitions). * @param name - Name of the property. * @param out The object or null. */ static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** * Calls the random number generator assigned to this ScriptInterface instance and returns the generated number. */ bool MathRandom(double& nbr) const; /** * JSNative wrapper of the above. */ static bool Math_random(JSContext* cx, uint argc, JS::Value* vp); /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } private: bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; CmptPrivate m_CmptPrivate; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; std::map m_CustomObjectTypes; }; // Explicitly instantiate void* as that is used for the generic template, // and we want to define it in the .cpp file. template <> void* ScriptInterface::ObjectFromCBData(const ScriptRequest& rq); template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return Script::FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/Simulation2.cpp =================================================================== --- ps/trunk/source/simulation2/Simulation2.cpp (revision 26273) +++ ps/trunk/source/simulation2/Simulation2.cpp (revision 26274) @@ -1,1002 +1,1001 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Simulation2.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "scriptinterface/StructuredClone.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpTemplateManager.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_util.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/Util.h" #include "ps/XML/Xeromyces.h" #include #include #include class CSimulation2Impl { public: CSimulation2Impl(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain) : m_SimContext(), m_ComponentManager(m_SimContext, cx), m_EnableOOSLog(false), m_EnableSerializationTest(false), m_RejoinTestTurn(-1), m_TestingRejoin(false), m_MapSettings(cx->GetGeneralJSContext()), m_InitAttributes(cx->GetGeneralJSContext()) { m_SimContext.m_UnitManager = unitManager; m_SimContext.m_Terrain = terrain; m_ComponentManager.LoadComponentTypes(); RegisterFileReloadFunc(ReloadChangedFileCB, this); // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("ooslog", m_EnableOOSLog); CFG_GET_VAL("serializationtest", m_EnableSerializationTest); CFG_GET_VAL("rejointest", m_RejoinTestTurn); if (m_RejoinTestTurn < 0) // Handle bogus values of the arg m_RejoinTestTurn = -1; } if (m_EnableOOSLog) { m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m_OOSLogPath.string8().c_str()); } } ~CSimulation2Impl() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); } void ResetState(bool skipScriptedComponents, bool skipAI) { m_DeltaTime = 0.0; m_LastFrameOffset = 0.0f; m_TurnNumber = 0; ResetComponentState(m_ComponentManager, skipScriptedComponents, skipAI); } static void ResetComponentState(CComponentManager& componentManager, bool skipScriptedComponents, bool skipAI) { componentManager.ResetState(); componentManager.InitSystemEntity(); componentManager.AddSystemComponents(skipScriptedComponents, skipAI); } static bool LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts); static bool LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path); static bool LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts); Status ReloadChangedFile(const VfsPath& path); static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } int ProgressiveLoad(); void Update(int turnLength, const std::vector& commands); static void UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands); void Interpolate(float simFrameLength, float frameOffset, float realFrameLength); void DumpState(); CSimContext m_SimContext; CComponentManager m_ComponentManager; double m_DeltaTime; float m_LastFrameOffset; std::string m_StartupScript; JS::PersistentRootedValue m_InitAttributes; JS::PersistentRootedValue m_MapSettings; std::set m_LoadedScripts; uint32_t m_TurnNumber; bool m_EnableOOSLog; OsPath m_OOSLogPath; // Functions and data for the serialization test mode: (see Update() for relevant comments) bool m_EnableSerializationTest; int m_RejoinTestTurn; bool m_TestingRejoin; // Secondary simulation (NB: order matters for destruction). std::unique_ptr m_SecondaryComponentManager; std::unique_ptr m_SecondaryTerrain; std::unique_ptr m_SecondaryContext; std::unique_ptr> m_SecondaryLoadedScripts; struct SerializationTestState { std::stringstream state; std::stringstream debug; std::string hash; }; void DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix); void ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter); void InitRNGSeedSimulation(); void InitRNGSeedAI(); static std::vector CloneCommandsFromOtherCompartment(const ScriptInterface& newScript, const ScriptInterface& oldScript, const std::vector& commands) { std::vector newCommands; newCommands.reserve(commands.size()); ScriptRequest rqNew(newScript); for (const SimulationCommand& command : commands) { JS::RootedValue tmpCommand(rqNew.cx, Script::CloneValueFromOtherCompartment(newScript, oldScript, command.data)); Script::FreezeObject(rqNew, tmpCommand, true); SimulationCommand cmd(command.player, rqNew.cx, tmpCommand); newCommands.emplace_back(std::move(cmd)); } return newCommands; } }; bool CSimulation2Impl::LoadDefaultScripts(CComponentManager& componentManager, std::set* loadedScripts) { return ( LoadScripts(componentManager, loadedScripts, L"simulation/components/interfaces/") && LoadScripts(componentManager, loadedScripts, L"simulation/helpers/") && LoadScripts(componentManager, loadedScripts, L"simulation/components/") ); } bool CSimulation2Impl::LoadScripts(CComponentManager& componentManager, std::set* loadedScripts, const VfsPath& path) { VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, path, L"*.js", pathnames) < 0) return false; bool ok = true; for (const VfsPath& scriptPath : pathnames) { if (loadedScripts) loadedScripts->insert(scriptPath); LOGMESSAGE("Loading simulation script '%s'", scriptPath.string8()); if (!componentManager.LoadScript(scriptPath)) ok = false; } return ok; } bool CSimulation2Impl::LoadTriggerScripts(CComponentManager& componentManager, JS::HandleValue mapSettings, std::set* loadedScripts) { bool ok = true; ScriptRequest rq(componentManager.GetScriptInterface()); if (Script::HasProperty(rq, mapSettings, "TriggerScripts")) { std::vector scriptNames; Script::GetProperty(rq, mapSettings, "TriggerScripts", scriptNames); for (const std::string& triggerScript : scriptNames) { std::string scriptName = "maps/" + triggerScript; if (loadedScripts) { if (loadedScripts->find(scriptName) != loadedScripts->end()) continue; loadedScripts->insert(scriptName); } LOGMESSAGE("Loading trigger script '%s'", scriptName.c_str()); if (!componentManager.LoadScript(scriptName.data())) ok = false; } } return ok; } Status CSimulation2Impl::ReloadChangedFile(const VfsPath& path) { // Ignore if this file wasn't loaded as a script // (TODO: Maybe we ought to load in any new .js files that are created in the right directories) if (m_LoadedScripts.find(path) == m_LoadedScripts.end()) return INFO::OK; // If the file doesn't exist (e.g. it was deleted), don't bother loading it since that'll give an error message. // (Also don't bother trying to 'unload' it from the component manager, because that's not possible) if (!VfsFileExists(path)) return INFO::OK; LOGMESSAGE("Reloading simulation script '%s'", path.string8()); if (!m_ComponentManager.LoadScript(path, true)) return ERR::FAIL; return INFO::OK; } int CSimulation2Impl::ProgressiveLoad() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; int ret; do { bool progressed = false; int total = 0; int progress = 0; CMessageProgressiveLoad msg(&progressed, &total, &progress); m_ComponentManager.BroadcastMessage(msg); if (!progressed || total == 0) return 0; // we have nothing left to load ret = Clamp(100*progress / total, 1, 100); } while (timer_Time() < end_time); return ret; } void CSimulation2Impl::DumpSerializationTestState(SerializationTestState& state, const OsPath& path, const OsPath::String& suffix) { if (!state.hash.empty()) { std::ofstream file (OsString(path / (L"hash." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << Hexify(state.hash); } if (!state.debug.str().empty()) { std::ofstream file (OsString(path / (L"debug." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc); file << state.debug.str(); } if (!state.state.str().empty()) { std::ofstream file (OsString(path / (L"state." + suffix)).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); file << state.state.str(); } } void CSimulation2Impl::ReportSerializationFailure( SerializationTestState* primaryStateBefore, SerializationTestState* primaryStateAfter, SerializationTestState* secondaryStateBefore, SerializationTestState* secondaryStateAfter) { const OsPath path = createDateIndexSubdirectory(psLogDir() / "serializationtest"); debug_printf("Writing serializationtest-data to %s\n", path.string8().c_str()); // Clean up obsolete files from previous runs wunlink(path / "hash.before.a"); wunlink(path / "hash.before.b"); wunlink(path / "debug.before.a"); wunlink(path / "debug.before.b"); wunlink(path / "state.before.a"); wunlink(path / "state.before.b"); wunlink(path / "hash.after.a"); wunlink(path / "hash.after.b"); wunlink(path / "debug.after.a"); wunlink(path / "debug.after.b"); wunlink(path / "state.after.a"); wunlink(path / "state.after.b"); if (primaryStateBefore) DumpSerializationTestState(*primaryStateBefore, path, L"before.a"); if (primaryStateAfter) DumpSerializationTestState(*primaryStateAfter, path, L"after.a"); if (secondaryStateBefore) DumpSerializationTestState(*secondaryStateBefore, path, L"before.b"); if (secondaryStateAfter) DumpSerializationTestState(*secondaryStateAfter, path, L"after.b"); debug_warn(L"Serialization test failure"); } void CSimulation2Impl::InitRNGSeedSimulation() { u32 seed = 0; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "Seed") || !Script::GetProperty(rq, m_MapSettings, "Seed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedSimulation: No seed value specified - using %d", seed); m_ComponentManager.SetRNGSeed(seed); } void CSimulation2Impl::InitRNGSeedAI() { u32 seed = 0; ScriptRequest rq(m_ComponentManager.GetScriptInterface()); if (!Script::HasProperty(rq, m_MapSettings, "AISeed") || !Script::GetProperty(rq, m_MapSettings, "AISeed", seed)) LOGWARNING("CSimulation2Impl::InitRNGSeedAI: No seed value specified - using %d", seed); CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); if (cmpAIManager) cmpAIManager->SetRNGSeed(seed); } void CSimulation2Impl::Update(int turnLength, const std::vector& commands) { PROFILE3("sim update"); PROFILE2_ATTR("turn %d", (int)m_TurnNumber); fixed turnLengthFixed = fixed::FromInt(turnLength) / 1000; /* * In serialization test mode, we save the original (primary) simulation state before each turn update. * We run the update, then load the saved state into a secondary context. * We serialize that again and compare to the original serialization (to check that * serialize->deserialize->serialize is equivalent to serialize). * Then we run the update on the secondary context, and check that its new serialized * state matches the primary context after the update (to check that the simulation doesn't depend * on anything that's not serialized). * * In rejoin test mode, the secondary simulation is initialized from serialized data at turn N, then both * simulations run independantly while comparing their states each turn. This is way faster than a * complete serialization test and allows us to reproduce OOSes on rejoin. */ const bool serializationTestDebugDump = false; // set true to save human-readable state dumps before an error is detected, for debugging (but slow) const bool serializationTestHash = true; // set true to save and compare hash of state SerializationTestState primaryStateBefore; const ScriptInterface& scriptInterface = m_ComponentManager.GetScriptInterface(); const bool startRejoinTest = (int64_t) m_RejoinTestTurn == m_TurnNumber; if (startRejoinTest) m_TestingRejoin = true; if (m_EnableSerializationTest || m_TestingRejoin) { ENSURE(m_ComponentManager.SerializeState(primaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_ComponentManager.DumpDebugState(primaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateBefore.hash, false)); } UpdateComponents(m_SimContext, turnLengthFixed, commands); if (m_EnableSerializationTest || startRejoinTest) { if (startRejoinTest) debug_printf("Initializing the secondary simulation\n"); m_SecondaryTerrain = std::make_unique(); m_SecondaryContext = std::make_unique(); m_SecondaryContext->m_Terrain = m_SecondaryTerrain.get(); m_SecondaryComponentManager = std::make_unique(*m_SecondaryContext, scriptInterface.GetContext()); m_SecondaryComponentManager->LoadComponentTypes(); m_SecondaryLoadedScripts = std::make_unique>(); ENSURE(LoadDefaultScripts(*m_SecondaryComponentManager, m_SecondaryLoadedScripts.get())); ResetComponentState(*m_SecondaryComponentManager, false, false); ScriptRequest rq(scriptInterface); // Load the trigger scripts after we have loaded the simulation. { ScriptRequest rq2(m_SecondaryComponentManager->GetScriptInterface()); JS::RootedValue mapSettingsCloned(rq2.cx, Script::CloneValueFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, m_MapSettings)); ENSURE(LoadTriggerScripts(*m_SecondaryComponentManager, mapSettingsCloned, m_SecondaryLoadedScripts.get())); } // Load the map into the secondary simulation LDR_BeginRegistering(); std::unique_ptr mapReader = std::make_unique(); std::string mapType; Script::GetProperty(rq, m_InitAttributes, "mapType", mapType); if (mapType == "random") { // TODO: support random map scripts debug_warn(L"Serialization test mode does not support random maps"); } else { std::wstring mapFile; Script::GetProperty(rq, m_InitAttributes, "map", mapFile); VfsPath mapfilename = VfsPath(mapFile).ChangeExtension(L".pmp"); mapReader->LoadMap(mapfilename, *scriptInterface.GetContext(), JS::UndefinedHandleValue, m_SecondaryTerrain.get(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, m_SecondaryContext.get(), INVALID_PLAYER, true); // throws exception on failure } LDR_EndRegistering(); ENSURE(LDR_NonprogressiveLoad() == INFO::OK); ENSURE(m_SecondaryComponentManager->DeserializeState(primaryStateBefore.state)); } if (m_EnableSerializationTest || m_TestingRejoin) { SerializationTestState secondaryStateBefore; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateBefore.state)); if (serializationTestDebugDump) ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateBefore.debug, false)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateBefore.hash, false)); if (primaryStateBefore.state.str() != secondaryStateBefore.state.str() || primaryStateBefore.hash != secondaryStateBefore.hash) { ReportSerializationFailure(&primaryStateBefore, NULL, &secondaryStateBefore, NULL); } SerializationTestState primaryStateAfter; ENSURE(m_ComponentManager.SerializeState(primaryStateAfter.state)); if (serializationTestHash) ENSURE(m_ComponentManager.ComputeStateHash(primaryStateAfter.hash, false)); UpdateComponents(*m_SecondaryContext, turnLengthFixed, CloneCommandsFromOtherCompartment(m_SecondaryComponentManager->GetScriptInterface(), scriptInterface, commands)); SerializationTestState secondaryStateAfter; ENSURE(m_SecondaryComponentManager->SerializeState(secondaryStateAfter.state)); if (serializationTestHash) ENSURE(m_SecondaryComponentManager->ComputeStateHash(secondaryStateAfter.hash, false)); if (primaryStateAfter.state.str() != secondaryStateAfter.state.str() || primaryStateAfter.hash != secondaryStateAfter.hash) { // Only do the (slow) dumping now we know we're going to need to report it ENSURE(m_ComponentManager.DumpDebugState(primaryStateAfter.debug, false)); ENSURE(m_SecondaryComponentManager->DumpDebugState(secondaryStateAfter.debug, false)); ReportSerializationFailure(&primaryStateBefore, &primaryStateAfter, &secondaryStateBefore, &secondaryStateAfter); } } // Run the GC occasionally // No delay because a lot of garbage accumulates in one turn and in non-visual replays there are // much more turns in the same time than in normal games. // Every 500 turns we run a shrinking GC, which decommits unused memory and frees all JIT code. // Based on testing, this seems to be a good compromise between memory usage and performance. // Also check the comment about gcPreserveCode in the ScriptInterface code and this forum topic: // http://www.wildfiregames.com/forum/index.php?showtopic=18466&p=300323 // // (TODO: we ought to schedule this for a frame where we're not // running the sim update, to spread the load) if (m_TurnNumber % 500 == 0) scriptInterface.GetContext()->ShrinkingGC(); else scriptInterface.GetContext()->MaybeIncrementalGC(0.0f); if (m_EnableOOSLog) DumpState(); - // Start computing AI for the next turn - CmpPtr cmpAIManager(m_SimContext, SYSTEM_ENTITY); - if (cmpAIManager) - cmpAIManager->StartComputation(); - ++m_TurnNumber; } void CSimulation2Impl::UpdateComponents(CSimContext& simContext, fixed turnLengthFixed, const std::vector& commands) { // TODO: the update process is pretty ugly, with lots of messages and dependencies // between different components. Ought to work out a nicer way to do this. CComponentManager& componentManager = simContext.GetComponentManager(); CmpPtr cmpPathfinder(simContext, SYSTEM_ENTITY); if (cmpPathfinder) cmpPathfinder->SendRequestedPaths(); { PROFILE2("Sim - Update Start"); CMessageTurnStart msgTurnStart; componentManager.BroadcastMessage(msgTurnStart); } - // Push AI commands onto the queue before we use them - CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); - if (cmpAIManager) - cmpAIManager->PushCommands(); CmpPtr cmpCommandQueue(simContext, SYSTEM_ENTITY); if (cmpCommandQueue) cmpCommandQueue->FlushTurn(commands); // Process newly generated move commands so the UI feels snappy if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } // Send all the update phases { PROFILE2("Sim - Update"); CMessageUpdate msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { CMessageUpdate_MotionFormation msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Process move commands for formations (group proxy) if (cmpPathfinder) { cmpPathfinder->StartProcessingMoves(true); cmpPathfinder->SendRequestedPaths(); } { PROFILE2("Sim - Motion Unit"); CMessageUpdate_MotionUnit msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } { PROFILE2("Sim - Update Final"); CMessageUpdate_Final msgUpdate(turnLengthFixed); componentManager.BroadcastMessage(msgUpdate); } // Clean up any entities destroyed during the simulation update componentManager.FlushDestroyedComponents(); + // Compute AI immediately at turn's end. + CmpPtr cmpAIManager(simContext, SYSTEM_ENTITY); + if (cmpAIManager) + { + cmpAIManager->StartComputation(); + cmpAIManager->PushCommands(); + } + // Process all remaining moves if (cmpPathfinder) { cmpPathfinder->UpdateGrid(); cmpPathfinder->StartProcessingMoves(false); } } void CSimulation2Impl::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { PROFILE3("sim interpolate"); m_LastFrameOffset = frameOffset; CMessageInterpolate msg(simFrameLength, frameOffset, realFrameLength); m_ComponentManager.BroadcastMessage(msg); // Clean up any entities destroyed during interpolate (e.g. local corpses) m_ComponentManager.FlushDestroyedComponents(); } void CSimulation2Impl::DumpState() { PROFILE("DumpState"); std::stringstream name;\ name << std::setw(5) << std::setfill('0') << m_TurnNumber << ".txt"; const OsPath path = m_OOSLogPath / name.str(); std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); if (!DirectoryExists(m_OOSLogPath)) { LOGWARNING("OOS-log directory %s was deleted, creating it again.", m_OOSLogPath.string8().c_str()); CreateDirectories(m_OOSLogPath, 0700); } file << "State hash: " << std::hex; std::string hashRaw; m_ComponentManager.ComputeStateHash(hashRaw, false); for (size_t i = 0; i < hashRaw.size(); ++i) file << std::setfill('0') << std::setw(2) << (int)(unsigned char)hashRaw[i]; file << std::dec << "\n"; file << "\n"; m_ComponentManager.DumpDebugState(file, true); std::ofstream binfile (OsString(path.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary); m_ComponentManager.SerializeState(binfile); } //////////////////////////////////////////////////////////////// CSimulation2::CSimulation2(CUnitManager* unitManager, std::shared_ptr cx, CTerrain* terrain) : m(new CSimulation2Impl(unitManager, cx, terrain)) { } CSimulation2::~CSimulation2() { delete m; } // Forward all method calls to the appropriate CSimulation2Impl/CComponentManager methods: void CSimulation2::EnableSerializationTest() { m->m_EnableSerializationTest = true; } void CSimulation2::EnableRejoinTest(int rejoinTestTurn) { m->m_RejoinTestTurn = rejoinTestTurn; } void CSimulation2::EnableOOSLog() { if (m->m_EnableOOSLog) return; m->m_EnableOOSLog = true; m->m_OOSLogPath = createDateIndexSubdirectory(psLogDir() / "oos_logs"); debug_printf("Writing ooslogs to %s\n", m->m_OOSLogPath.string8().c_str()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity()); } entity_id_t CSimulation2::AddEntity(const std::wstring& templateName, entity_id_t preferredId) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewEntity(preferredId)); } entity_id_t CSimulation2::AddLocalEntity(const std::wstring& templateName) { return m->m_ComponentManager.AddEntity(templateName, m->m_ComponentManager.AllocateNewLocalEntity()); } void CSimulation2::DestroyEntity(entity_id_t ent) { m->m_ComponentManager.DestroyComponentsSoon(ent); } void CSimulation2::FlushDestroyedEntities() { m->m_ComponentManager.FlushDestroyedComponents(); } IComponent* CSimulation2::QueryInterface(entity_id_t ent, int iid) const { return m->m_ComponentManager.QueryInterface(ent, iid); } void CSimulation2::PostMessage(entity_id_t ent, const CMessage& msg) const { m->m_ComponentManager.PostMessage(ent, msg); } void CSimulation2::BroadcastMessage(const CMessage& msg) const { m->m_ComponentManager.BroadcastMessage(msg); } CSimulation2::InterfaceList CSimulation2::GetEntitiesWithInterface(int iid) { return m->m_ComponentManager.GetEntitiesWithInterface(iid); } const CSimulation2::InterfaceListUnordered& CSimulation2::GetEntitiesWithInterfaceUnordered(int iid) { return m->m_ComponentManager.GetEntitiesWithInterfaceUnordered(iid); } const CSimContext& CSimulation2::GetSimContext() const { return m->m_SimContext; } ScriptInterface& CSimulation2::GetScriptInterface() const { return m->m_ComponentManager.GetScriptInterface(); } void CSimulation2::PreInitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "PreInitGame"); } void CSimulation2::InitGame() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue settings(rq.cx); JS::RootedValue tmpInitAttributes(rq.cx, GetInitAttributes()); Script::GetProperty(rq, tmpInitAttributes, "settings", &settings); ScriptFunction::CallVoid(rq, global, "InitGame", settings); } void CSimulation2::Update(int turnLength) { std::vector commands; m->Update(turnLength, commands); } void CSimulation2::Update(int turnLength, const std::vector& commands) { m->Update(turnLength, commands); } void CSimulation2::Interpolate(float simFrameLength, float frameOffset, float realFrameLength) { m->Interpolate(simFrameLength, frameOffset, realFrameLength); } void CSimulation2::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { PROFILE3("sim submit"); CMessageRenderSubmit msg(collector, frustum, culling); m->m_ComponentManager.BroadcastMessage(msg); } float CSimulation2::GetLastFrameOffset() const { return m->m_LastFrameOffset; } bool CSimulation2::LoadScripts(const VfsPath& path) { return m->LoadScripts(m->m_ComponentManager, &m->m_LoadedScripts, path); } bool CSimulation2::LoadDefaultScripts() { return m->LoadDefaultScripts(m->m_ComponentManager, &m->m_LoadedScripts); } void CSimulation2::SetStartupScript(const std::string& code) { m->m_StartupScript = code; } const std::string& CSimulation2::GetStartupScript() { return m->m_StartupScript; } void CSimulation2::SetInitAttributes(JS::HandleValue attribs) { m->m_InitAttributes = attribs; } JS::Value CSimulation2::GetInitAttributes() { return m->m_InitAttributes.get(); } void CSimulation2::GetInitAttributes(JS::MutableHandleValue ret) { ret.set(m->m_InitAttributes); } void CSimulation2::SetMapSettings(const std::string& settings) { Script::ParseJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), settings, &m->m_MapSettings); } void CSimulation2::SetMapSettings(JS::HandleValue settings) { m->m_MapSettings = settings; m->InitRNGSeedSimulation(); m->InitRNGSeedAI(); } std::string CSimulation2::GetMapSettingsString() { return Script::StringifyJSON(ScriptRequest(m->m_ComponentManager.GetScriptInterface()), &m->m_MapSettings); } void CSimulation2::GetMapSettings(JS::MutableHandleValue ret) { ret.set(m->m_MapSettings); } void CSimulation2::LoadPlayerSettings(bool newPlayers) { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); ScriptFunction::CallVoid(rq, global, "LoadPlayerSettings", m->m_MapSettings, newPlayers); } void CSimulation2::LoadMapSettings() { ScriptRequest rq(GetScriptInterface()); JS::RootedValue global(rq.cx, rq.globalValue()); // Initialize here instead of in Update() ScriptFunction::CallVoid(rq, global, "LoadMapSettings", m->m_MapSettings); Script::FreezeObject(rq, m->m_InitAttributes, true); GetScriptInterface().SetGlobal("InitAttributes", m->m_InitAttributes, true, true, true); if (!m->m_StartupScript.empty()) GetScriptInterface().LoadScript(L"map startup script", m->m_StartupScript); // Load the trigger scripts after we have loaded the simulation and the map. m->LoadTriggerScripts(m->m_ComponentManager, m->m_MapSettings, &m->m_LoadedScripts); } int CSimulation2::ProgressiveLoad() { return m->ProgressiveLoad(); } Status CSimulation2::ReloadChangedFile(const VfsPath& path) { return m->ReloadChangedFile(path); } void CSimulation2::ResetState(bool skipScriptedComponents, bool skipAI) { m->ResetState(skipScriptedComponents, skipAI); } bool CSimulation2::ComputeStateHash(std::string& outHash, bool quick) { return m->m_ComponentManager.ComputeStateHash(outHash, quick); } bool CSimulation2::DumpDebugState(std::ostream& stream) { stream << "sim turn: " << m->m_TurnNumber << std::endl; return m->m_ComponentManager.DumpDebugState(stream, true); } bool CSimulation2::SerializeState(std::ostream& stream) { return m->m_ComponentManager.SerializeState(stream); } bool CSimulation2::DeserializeState(std::istream& stream) { // TODO: need to make sure the required SYSTEM_ENTITY components get constructed return m->m_ComponentManager.DeserializeState(stream); } void CSimulation2::ActivateRejoinTest(int turn) { if (m->m_RejoinTestTurn != -1) return; LOGMESSAGERENDER("Rejoin test will activate in %i turns", turn - m->m_TurnNumber); m->m_RejoinTestTurn = turn; } std::string CSimulation2::GenerateSchema() { return m->m_ComponentManager.GenerateSchema(); } static std::vector GetJSONData(const VfsPath& path) { VfsPaths pathnames; Status ret = vfs::GetPathnames(g_VFS, path, L"*.json", pathnames); if (ret != INFO::OK) { // Some error reading directory wchar_t error[200]; LOGERROR("Error reading directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return std::vector(); } std::vector data; for (const VfsPath& p : pathnames) { // Load JSON file CVFSFile file; PSRETURN loadStatus = file.Load(g_VFS, p); if (loadStatus != PSRETURN_OK) { LOGERROR("GetJSONData: Failed to load file '%s': %s", p.string8(), GetErrorString(loadStatus)); continue; } data.push_back(file.DecodeUTF8()); // assume it's UTF-8 } return data; } std::vector CSimulation2::GetRMSData() { return GetJSONData(L"maps/random/"); } std::vector CSimulation2::GetCivData() { return GetJSONData(L"simulation/data/civs/"); } std::vector CSimulation2::GetVictoryConditiondData() { return GetJSONData(L"simulation/data/settings/victory_conditions/"); } static std::string ReadJSON(const VfsPath& path) { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return std::string(); } // Load JSON file CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return std::string(); } return file.DecodeUTF8(); // assume it's UTF-8 } std::string CSimulation2::GetPlayerDefaults() { return ReadJSON(L"simulation/data/settings/player_defaults.json"); } std::string CSimulation2::GetMapSizes() { return ReadJSON(L"simulation/data/settings/map_sizes.json"); } std::string CSimulation2::GetAIData() { const ScriptInterface& scriptInterface = GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue aiData(rq.cx, ICmpAIManager::GetAIs(scriptInterface)); // Build single JSON string with array of AI data JS::RootedValue ais(rq.cx); if (!Script::CreateObject(rq, &ais, "AIData", aiData)) return std::string(); return Script::StringifyJSON(rq, &ais); } Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 26273) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 26274) @@ -1,1105 +1,1120 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 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 "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/StructuredClone.h" #include "scriptinterface/JSON.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/ICmpTerritoryManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/SerializedTypes.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" extern void QuitEngine(); /** * @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). + * The original idea was to run CAIWorker in a separate thread to prevent + * slow AIs from impacting framerate. However, copying the game-state every turn + * proved difficult and rather slow itself (and isn't threadable, obviously). + * For these reasons, the design was changed to a single-thread, same-compartment, different-realm design. + * The AI can therefore directly use the simulation data via the 'Sim' & 'SimEngine' globals. + * As a result, a lof of the code is still designed to be "thread-ready", but this no longer matters. * - * 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. + * TODO: despite the above, it would still be useful to allow the AI to run tasks asynchronously (and off-thread). + * This could be implemented by having a separate JS runtime in a different thread, + * that runs tasks and returns after a distinct # of simulation turns (to maintain determinism). * - * JS::Values are passed between the game and AI threads using Script::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. + * Note also that the RL Interface, by default, uses the 'AI representation'. + * This representation, alimented by the JS AIInterface/AIProxy tandem, is likely to grow smaller over time + * as the AI uses more sim data directly. */ /** - * Implements worker thread for CCmpAIManager. + * AI computation orchestator 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, std::shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetGeneralJSContext()) { } 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; ScriptRequest rq(m_ScriptInterface); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(rq.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(rq.cx); // object that should contain the constructor function JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!Script::HasProperty(rq, metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } Script::GetProperty(rq, metadata, "moduleName", moduleName); if (!Script::GetProperty(rq, 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 (!Script::GetProperty(rq, 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 (!Script::GetProperty(rq, objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } Script::GetProperty(rq, metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); Script::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); Script::SetProperty(rq, settings, "templates", m_Worker.m_EntityTemplates, false); } JS::RootedValueVector argv(rq.cx); ignore_result(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(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, 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(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); ScriptRequest rq(m_ScriptInterface); ScriptFunction::CallVoid(rq, 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 context destructor. std::shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; - std::vector m_Commands; + std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; - std::vector commands; + std::vector commands; }; CAIWorker() : - m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), - m_HasSharedComponent(false), - m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()), - m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()), - m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()), - m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext()) + m_HasSharedComponent(false) + { + } + + ~CAIWorker() + { + // Init will always be called. + JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); + } + + void Init(const ScriptInterface& simInterface) { + // Create the script interface in the same compartment as the simulation interface. + // This will allow us to directly share data from the sim to the AI (and vice versa, should the need arise). + m_ScriptInterface = std::make_shared("Engine", "AI", simInterface); + + ScriptRequest rq(m_ScriptInterface); + + m_EntityTemplates.init(rq.cx); + m_SharedAIObj.init(rq.cx); + m_PassabilityMapVal.init(rq.cx); + m_TerritoryMapVal.init(rq.cx); + m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); - ScriptRequest rq(m_ScriptInterface); + { + ScriptRequest simrq(simInterface); + // Register the sim globals for easy & explicit access. Mark it replaceable for hotloading. + JS::RootedValue global(rq.cx, simrq.globalValue()); + m_ScriptInterface->SetGlobal("Sim", global, true); + JS::RootedValue scope(rq.cx, JS::ObjectValue(*simrq.nativeScope.get())); + m_ScriptInterface->SetGlobal("SimEngine", scope, true); + } + #define REGISTER_FUNC_NAME(func, name) \ ScriptFunction::Register<&CAIWorker::func, ScriptInterface::ObjectFromCBData>(rq, name); REGISTER_FUNC_NAME(PostCommand, "PostCommand"); REGISTER_FUNC_NAME(LoadScripts, "IncludeModule"); ScriptFunction::Register(rq, "Exit"); REGISTER_FUNC_NAME(ComputePathScript, "ComputePath"); REGISTER_FUNC_NAME(DumpImage, "DumpImage"); REGISTER_FUNC_NAME(GetTemplate, "GetTemplate"); #undef REGISTER_FUNC_NAME JSI_VFS::RegisterScriptFunctions_Simulation(rq); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); - } - ~CAIWorker() - { - JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), 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; } void PostCommand(int playerid, JS::HandleValue cmd) { ScriptRequest rq(m_ScriptInterface); for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(Script::WriteStructuredClone(rq, cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } JS::Value ComputePathScript(JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { ScriptRequest rq(m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); Script::FromJSVal(rq, position, pos); Script::FromJSVal(rq, goal, goalPos); ComputePath(pos, goalPos, passClass, waypoints); Script::ToJSVal(rq, &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(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret); for (Waypoint& wp : ret.m_Waypoints) waypoints.emplace_back(wp.x, wp.z); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ void DumpImage(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); std::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() { ScriptRequest rq(m_ScriptInterface); // 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(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!Script::GetProperty(rq, global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!Script::GetProperty(rq, 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 playersID(rq.cx); Script::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); Script::ToJSVal(rq, &val, m_Players[i]->m_Player); Script::SetPropertyInt(rq, playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); Script::CreateObject( rq, &settings, "players", playersID, "templates", m_EntityTemplates); JS::RootedValueVector argv(rq.cx); ignore_result(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) { std::shared_ptr ai = std::make_shared(*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 Script::StructuredClone& 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. ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); Script::ReadStructuredClone(rq, gameState, &state); Script::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); Script::ToJSVal(rq, &m_TerritoryMapVal, territoryMap); m_PassabilityMap = passabilityMap; m_NonPathfindingPassClasses = nonPathfindingPassClassMasks; m_PathfindingPassClasses = pathfindingPassClassMasks; m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); if (m_HasSharedComponent) { Script::SetProperty(rq, state, "passabilityMap", m_PassabilityMapVal, true); Script::SetProperty(rq, state, "territoryMap", m_TerritoryMapVal, true); ScriptFunction::CallVoid(rq, 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 Script::StructuredClone& 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); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } else { m_LongPathfinder.Update(&m_PassabilityMap); m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid); } ScriptRequest rq(m_ScriptInterface); if (dimensionChange || justDeserialized) Script::ToJSVal(rq, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, 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; ScriptRequest rq(m_ScriptInterface); if (dimensionChange) Script::ToJSVal(rq, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, 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 LoadEntityTemplates(const std::vector >& templates) { ScriptRequest rq(m_ScriptInterface); m_HasLoadedEntityTemplates = true; Script::CreateObject(rq, &m_EntityTemplates); JS::RootedValue val(rq.cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(rq, false, &val); Script::SetProperty(rq, 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); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { if (m_Players.empty()) return; ScriptRequest rq(m_ScriptInterface); 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) serializer.ScriptVal("sharedData", &m_SharedAIObj); 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(rq.cx); Script::ReadStructuredClone(rq, m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } serializer.ScriptVal("data", &m_Players[i]->m_Obj); } // AI pathfinder Serializer(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(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; ScriptRequest rq(m_ScriptInterface); 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(); deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj); } 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(rq.cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(Script::WriteStructuredClone(rq, val)); } deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj); } // AI pathfinder Serializer(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(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_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses); } int getPlayerSize() { return m_Players.size(); } private: static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc) { for (std::pair>& metadata : m_PlayerMetadata) JS::TraceEdge(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 Script::ReadJSONFile(ScriptRequest(m_ScriptInterface), 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 ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); Script::ReadStructuredClone(rq, m_GameState, &state); Script::SetProperty(rq, state, "passabilityMap", m_PassabilityMapVal, true); Script::SetProperty(rq, state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // Script::FreezeObject(rq, 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"); ScriptFunction::CallVoid(rq, 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 context destructor. - std::shared_ptr m_ScriptContext; - std::shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; JS::PersistentRootedValue m_EntityTemplates; bool m_HasLoadedEntityTemplates; 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; Script::StructuredClone m_GameState; Grid m_PassabilityMap; JS::PersistentRootedValue m_PassabilityMapVal; Grid m_TerritoryMap; JS::PersistentRootedValue m_TerritoryMapVal; std::map m_NonPathfindingPassClasses; std::map m_PathfindingPassClasses; HierarchicalPathfinder m_HierarchicalPathfinder; LongPathfinder m_LongPathfinder; bool m_CommandsComputed; 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_Worker.Init(GetSimContext().GetScriptInterface()); + 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() { m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); 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(rq.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 NeedUpdateAI first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID)) territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); LoadPathfinderClasses(state); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; if (cmpPathfinder) cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.RunGamestateInit(Script::WriteStructuredClone(rq, state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } virtual void StartComputation() { PROFILE("AI setup"); const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(rq.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(Script::WriteStructuredClone(rq, 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 NeedUpdateAI first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&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(); ScriptRequest rq(scriptInterface); JS::RootedValue clonedCommandVal(rq.cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { Script::ReadStructuredClone(rq, 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(); ScriptRequest rq(scriptInterface); JS::RootedValue classesVal(rq.cx); Script::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) Script::SetProperty(rq, classesVal, it->first.c_str(), it->second, true); Script::SetProperty(rq, state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager)