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