Index: ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js =================================================================== --- ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js (revision 24461) +++ ps/trunk/binaries/data/mods/_test.sim/simulation/components/test-serialize.js (revision 24462) @@ -1,90 +1,94 @@ function TestScript1_values() {} TestScript1_values.prototype.Init = function() { this.x = +this.template.x; this.str = "this is a string"; - this.things = { a: 1, b: "2", c: [3, "4", [5, []]] }; + this.things = { "a": 1, "b": "2", "c": [3, "4", [5, []]] }; }; TestScript1_values.prototype.GetX = function() { // print(uneval(this)); return this.x; }; Engine.RegisterComponentType(IID_Test1, "TestScript1_values", TestScript1_values); // -------- // function TestScript1_entity() {} TestScript1_entity.prototype.GetX = function() { // Test that .entity is readonly try { delete this.entity; Engine.TS_FAIL("Missed exception"); - } catch (e) { } + } catch (e) { /* OK */ } try { this.entity = -1; Engine.TS_FAIL("Missed exception"); - } catch (e) { } + } catch (e) { /* OK */ } // and return the value return this.entity; }; Engine.RegisterComponentType(IID_Test1, "TestScript1_entity", TestScript1_entity); // -------- // function TestScript1_nontree() {} TestScript1_nontree.prototype.Init = function() { var n = [1]; - this.x = [n, n, null, { y: n }]; + this.x = [n, n, null, { "y": n }]; this.x[2] = this.x; }; TestScript1_nontree.prototype.GetX = function() { // print(uneval(this)+"\n"); this.x[0][0] += 1; return this.x[0][0] + this.x[1][0] + this.x[2][0][0] + this.x[3].y[0]; }; Engine.RegisterComponentType(IID_Test1, "TestScript1_nontree", TestScript1_nontree); // -------- // function TestScript1_custom() {} TestScript1_custom.prototype.Init = function() { this.y = 2; }; TestScript1_custom.prototype.Serialize = function() { - return {c:1}; + return { "c": 1 }; +}; + +TestScript1_custom.prototype.Deserialize = function(data) { + this.c = data.c; }; Engine.RegisterComponentType(IID_Test1, "TestScript1_custom", TestScript1_custom); // -------- // function TestScript1_getter() {} TestScript1_getter.prototype.Init = function() { this.x = 100; - this.__defineGetter__('x', function () { print("FAIL\n"); die(); return 200; }); + this.__defineGetter__('x', function() { print("FAIL\n"); die(); return 200; }); }; Engine.RegisterComponentType(IID_Test1, "TestScript1_getter", TestScript1_getter); // -------- // function TestScript1_consts() {} TestScript1_consts.prototype.Schema = ""; TestScript1_consts.prototype.GetX = function() { return (+this.entity) + (+this.template.x); }; Engine.RegisterComponentType(IID_Test1, "TestScript1_consts", TestScript1_consts); Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js (revision 24462) @@ -0,0 +1,9 @@ +function test_serialization() +{ + let test_val = new Vector2D(1, 2); + let rt = Engine.SerializationRoundTrip(test_val); + TS_ASSERT_EQUALS(test_val.constructor, rt.constructor); + TS_ASSERT_EQUALS(rt.add(test_val).x, 2); +} + +test_serialization(); Property changes on: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_vector.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 24461) +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js (revision 24462) @@ -1,641 +1,640 @@ // Ships respawn every few minutes, attack the closest warships, then patrol the sea. // To prevent unlimited spawning of ships, no more than the amount of ships intended at a given time are spawned. // Ships are filled or refilled with new units. // The number of ships, number of units per ship, as well as ratio of siege engines, champion and heroes // increases with time, while keeping an individual and randomized composition for each ship. // Each hero exists at most once per map. // Every few minutes, equal amount of ships unload units at the sides of the river unless // one side of the river was wiped from players. // Siege engines attack defensive structures, units attack units then patrol that side of the river. const showDebugLog = false; const danubiusAttackerTemplates = deepfreeze({ "ships": TriggerHelper.GetTemplateNamesByClasses("Warship", "gaul", undefined, undefined, true), "siege": TriggerHelper.GetTemplateNamesByClasses("Siege", "gaul", undefined, undefined, true), "females": TriggerHelper.GetTemplateNamesByClasses("FemaleCitizen", "gaul", undefined, undefined, true), "healers": TriggerHelper.GetTemplateNamesByClasses("Healer", "gaul", undefined, undefined, true), "champions": TriggerHelper.GetTemplateNamesByClasses("Champion", "gaul", undefined, undefined, true), "champion_infantry": TriggerHelper.GetTemplateNamesByClasses("Champion+Infantry", "gaul", undefined, undefined, true), "citizen_soldiers": TriggerHelper.GetTemplateNamesByClasses("CitizenSoldier", "gaul", undefined, "Basic", true), "heroes": [ // Excludes the Vercingetorix variant "units/gaul/hero_viridomarus", "units/gaul/hero_vercingetorix", "units/gaul/hero_brennus" ] }); var ccDefenders = [ { "count": 8, "templates": danubiusAttackerTemplates.citizen_soldiers }, { "count": 13, "templates": danubiusAttackerTemplates.champions }, { "count": 4, "templates": danubiusAttackerTemplates.healers }, { "count": 5, "templates": danubiusAttackerTemplates.females }, { "count": 10, "templates": ["gaia/fauna_sheep"] } ]; var gallicBuildingGarrison = [ { "buildingClasses": ["House"], "unitTemplates": danubiusAttackerTemplates.females.concat(danubiusAttackerTemplates.healers) }, { "buildingClasses": ["CivCentre", "Temple"], "unitTemplates": danubiusAttackerTemplates.champions, }, { "buildingClasses": ["Tower", "Outpost"], "unitTemplates": danubiusAttackerTemplates.champion_infantry } ]; /** * Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side. * However we want interaction and fights between the teams. * This can be accomplished by not wiping out players buildings entirely. */ /** * Time in minutes between two consecutive waves spawned from the gaia civic centers, if they still exist. */ var ccAttackerInterval = t => randFloat(6, 8); /** * Number of attackers spawned at a civic center at t minutes ingame time. */ var ccAttackerCount = t => Math.min(20, Math.max(0, Math.round(t * 1.5))); /** * Time between two consecutive waves. */ var shipRespawnTime = () => randFloat(8, 10); /** * Limit of ships on the map when spawning them. * Have at least two ships, so that both sides will be visited. */ var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers)); /** * Order all ships to ungarrison at the shoreline. */ var shipUngarrisonInterval = () => randFloat(5, 7); /** * Time between refillings of all ships with new soldiers. */ var shipFillInterval = () => randFloat(4, 5); /** * Total count of gaia attackers per shipload. */ var attackersPerShip = t => Math.min(30, Math.round(t * 2)); /** * Likelihood of adding a non-existing hero at t minutes. */ var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60)); /** * Percent of healers to add per shipload after potentially adding a hero and siege engines. */ var healerRatio = t => randFloat(0, 0.1); /** * Number of siege engines to add per shipload. */ var siegeCount = t => 1 + Math.min(2, Math.floor(t / 30)); /** * Percent of champions to be added after spawning heroes, healers and siege engines. * Rest will be citizen soldiers. */ var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75)); /** * Number of trigger points to patrol when not having enemies to attack. */ var patrolCount = 5; /** * Which units ships should focus when attacking and patrolling. */ var shipTargetClass = "Warship"; /** * Which entities siege engines should focus when attacking and patrolling. */ var siegeTargetClass = "Defensive"; /** * Which entities units should focus when attacking and patrolling. */ var unitTargetClass = "Unit+!Ship"; /** * Ungarrison ships when being in this range of the target. */ var shipUngarrisonDistance = 50; /** * Currently formations are not working properly and enemies in vision range are often ignored. * So only have a small chance of using formations. */ var formationProbability = 0.2; var unitFormations = [ "special/formations/box", "special/formations/battle_line", "special/formations/line_closed", "special/formations/column_closed" ]; /** * Chance for the units at the meeting place to participate in the ritual. */ var ritualProbability = 0.75; /** * Units celebrating at the meeting place will perform one of these animations * if idle and switch back when becoming idle again. */ var ritualAnimations = { "female": ["attack_slaughter"], "male": ["attack_capture", "promotion", "attack_slaughter"], "healer": ["attack_capture", "promotion", "heal"] }; var triggerPointShipSpawn = "A"; var triggerPointShipPatrol = "B"; var triggerPointUngarrisonLeft = "C"; var triggerPointUngarrisonRight = "D"; var triggerPointLandPatrolLeft = "E"; var triggerPointLandPatrolRight = "F"; var triggerPointCCAttackerPatrolLeft = "G"; var triggerPointCCAttackerPatrolRight = "H"; var triggerPointRiverDirection = "I"; /** * Which playerID to use for the opposing gallic reinforcements. */ var gaulPlayer = 0; Trigger.prototype.debugLog = function(txt) { if (showDebugLog) print("DEBUG [" + Math.round(TriggerHelper.GetMinutes()) + "] " + txt + "\n"); }; Trigger.prototype.GarrisonAllGallicBuildings = function() { this.debugLog("Garrisoning all gallic buildings"); for (let buildingGarrison of gallicBuildingGarrison) for (let buildingClass of buildingGarrison.buildingClasses) { let unitCounts = TriggerHelper.SpawnAndGarrisonAtClasses(gaulPlayer, buildingClass, buildingGarrison.unitTemplates, 1); this.debugLog("Garrisoning at " + buildingClass + ": " + uneval(unitCounts)); } }; /** * Spawn units of the template at each gaia Civic Center and set them to defensive. */ Trigger.prototype.SpawnInitialCCDefenders = function() { this.debugLog("To defend CCs, spawning " + uneval(ccDefenders)); for (let ent of this.civicCenters) for (let ccDefender of ccDefenders) for (let spawnedEnt of TriggerHelper.SpawnUnits(ent, pickRandom(ccDefender.templates), ccDefender.count, gaulPlayer)) TriggerHelper.SetUnitStance(spawnedEnt, "defensive"); }; Trigger.prototype.SpawnCCAttackers = function() { let time = TriggerHelper.GetMinutes(); let [spawnLeft, spawnRight] = this.GetActiveRiversides(); for (let gaiaCC of this.civicCenters) { if (!TriggerHelper.IsInWorld(gaiaCC)) continue; let isLeft = this.IsLeftRiverside(gaiaCC); if (isLeft && !spawnLeft || !isLeft && !spawnRight) continue; let templateCounts = TriggerHelper.BalancedTemplateComposition(this.GetAttackerComposition(time, false), ccAttackerCount(time)); this.debugLog("Spawning civic center attackers at " + gaiaCC + ": " + uneval(templateCounts)); let ccAttackers = []; for (let templateName in templateCounts) { let ents = TriggerHelper.SpawnUnits(gaiaCC, templateName, templateCounts[templateName], gaulPlayer); if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0]) this.heroes.add(ents[0]); ccAttackers = ccAttackers.concat(ents); } let patrolPointRef = isLeft ? triggerPointCCAttackerPatrolLeft : triggerPointCCAttackerPatrolRight; this.AttackAndPatrol(ccAttackers, unitTargetClass, patrolPointRef, "CCAttackers", false); } if (this.civicCenters.size) this.DoAfterDelay(ccAttackerInterval() * 60 * 1000, "SpawnCCAttackers", {}); }; /** * Remember most Humans present at the beginning of the match (before spawning any unit) and * make them defensive. */ Trigger.prototype.StartCelticRitual = function() { for (let ent of TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "Human")) { if (randBool(ritualProbability)) this.ritualEnts.add(ent); TriggerHelper.SetUnitStance(ent, "defensive"); } this.DoRepeatedly(5 * 1000, "UpdateCelticRitual", {}); }; /** * Play one of the given animations for most participants if and only if they are idle. */ Trigger.prototype.UpdateCelticRitual = function() { for (let ent of this.ritualEnts) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE") continue; let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); if (!cmpIdentity) continue; let animations = ritualAnimations[ cmpIdentity.HasClass("Healer") ? "healer" : cmpIdentity.HasClass("Female") ? "female" : "male"]; let cmpVisual = Engine.QueryInterface(ent, IID_Visual); if (!cmpVisual) continue; if (animations.indexOf(cmpVisual.GetAnimationName()) == -1) cmpVisual.SelectAnimation(pickRandom(animations), false, 1, ""); } }; /** * Spawn ships with a unique attacker composition each until * the number of ships is reached that is supposed to exist at the given time. */ Trigger.prototype.SpawnShips = function() { let time = TriggerHelper.GetMinutes(); let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetActivePlayers().length; let shipSpawnCount = shipCount(time, numPlayers) - this.ships.size; this.debugLog("Spawning " + shipSpawnCount + " ships"); while (this.ships.size < shipSpawnCount) this.ships.add( TriggerHelper.SpawnUnits( pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), pickRandom(danubiusAttackerTemplates.ships), 1, gaulPlayer)[0]); for (let ship of this.ships) this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ship", true); this.DoAfterDelay(shipRespawnTime(time) * 60 * 1000, "SpawnShips", {}); let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); cmpTimer.CancelTimer(this.fillShipsTimer); this.FillShips(); }; Trigger.prototype.GetAttackerComposition = function(time, siegeEngines) { let champRatio = championRatio(time); return [ { "templates": danubiusAttackerTemplates.heroes, "count": randBool(heroProbability(time)) ? 1 : 0, "unique_entities": Array.from(this.heroes) }, { "templates": danubiusAttackerTemplates.siege, "count": siegeEngines ? siegeCount(time) : 0 }, { "templates": danubiusAttackerTemplates.healers, "frequency": healerRatio(time) }, { "templates": danubiusAttackerTemplates.champions, "frequency": champRatio }, { "templates": danubiusAttackerTemplates.citizen_soldiers, "frequency": 1 - champRatio } ]; }; Trigger.prototype.FillShips = function() { let time = TriggerHelper.GetMinutes(); for (let ship of this.ships) { let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; let templateCounts = TriggerHelper.BalancedTemplateComposition( this.GetAttackerComposition(time, true), Math.max(0, attackersPerShip(time) - cmpGarrisonHolder.GetEntities().length)); this.debugLog("Filling ship " + ship + " with " + uneval(templateCounts)); for (let templateName in templateCounts) { let ents = TriggerHelper.SpawnGarrisonedUnits(ship, templateName, templateCounts[templateName], gaulPlayer); if (danubiusAttackerTemplates.heroes.indexOf(templateName) != -1 && ents[0]) this.heroes.add(ents[0]); } } this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {}); }; /** * Attack the closest enemy target around, then patrol the map. */ Trigger.prototype.AttackAndPatrol = function(entities, targetClass, triggerPointRef, debugName, attack) { if (!entities.length) return; let healers = TriggerHelper.MatchEntitiesByClass(entities, "Healer").filter(TriggerHelper.IsInWorld); if (healers.length) { let healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Hero Champion"); if (!healerTargets.length) healerTargets = TriggerHelper.MatchEntitiesByClass(entities, "Soldier"); ProcessCommand(gaulPlayer, { "type": "guard", "entities": healers, "target": pickRandom(healerTargets), "queued": false }); } let attackers = TriggerHelper.MatchEntitiesByClass(entities, "!Healer").filter(TriggerHelper.IsInWorld); if (!attackers.length) return; let isLeft = this.IsLeftRiverside(attackers[0]); let targets = TriggerHelper.MatchEntitiesByClass(TriggerHelper.GetAllPlayersEntities(), targetClass); let closestTarget; let minDistance = Infinity; for (let target of targets) { if (!TriggerHelper.IsInWorld(target) || this.IsLeftRiverside(target) != isLeft) continue; let targetDistance = PositionHelper.DistanceBetweenEntities(attackers[0], target); if (targetDistance < minDistance) { closestTarget = target; minDistance = targetDistance; } } this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(closestTarget)); if (attack && closestTarget) ProcessCommand(gaulPlayer, { "type": "attack", "entities": attackers, "target": closestTarget, "queued": true, "allowCapture": false }); let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount); this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets)); for (let patrolTarget of patrolTargets) { let targetPos = TriggerHelper.GetEntityPosition2D(patrolTarget); ProcessCommand(gaulPlayer, { "type": "patrol", "entities": attackers, "x": targetPos.x, "z": targetPos.y, "targetClasses": { "attack": targetClass }, "queued": true, "allowCapture": false }); } }; /** * To avoid unloading unlimited amounts of units on empty riversides, * only add attackers to riversides where player buildings exist that are * actually targeted. */ Trigger.prototype.GetActiveRiversides = function() { let left = false; let right = false; for (let ent of TriggerHelper.GetAllPlayersEntitiesByClass(siegeTargetClass)) { if (this.IsLeftRiverside(ent)) left = true; else right = true; if (left && right) break; } return [left, right]; }; Trigger.prototype.IsLeftRiverside = function(ent) { return Vector2D.sub(TriggerHelper.GetEntityPosition2D(ent), this.mapCenter).cross(this.riverDirection) < 0; }; /** * Order all ships to abort naval warfare and move to the shoreline all few minutes. */ Trigger.prototype.UngarrisonShipsOrder = function() { let [ungarrisonLeft, ungarrisonRight] = this.GetActiveRiversides(); if (!ungarrisonLeft && !ungarrisonRight) return; // Determine which ships should ungarrison on which side of the river let ships = Array.from(this.ships); let shipsLeft = []; let shipsRight = []; if (ungarrisonLeft && ungarrisonRight) { shipsLeft = shuffleArray(ships).slice(0, Math.round(ships.length / 2)); shipsRight = ships.filter(ship => shipsLeft.indexOf(ship) == -1); } else if (ungarrisonLeft) shipsLeft = ships; else if (ungarrisonRight) shipsRight = ships; // Determine which ships should ungarrison and patrol at which trigger point names let sides = []; if (shipsLeft.length) sides.push({ "ships": shipsLeft, "ungarrisonPointRef": triggerPointUngarrisonLeft, "landPointRef": triggerPointLandPatrolLeft }); if (shipsRight.length) sides.push({ "ships": shipsRight, "ungarrisonPointRef": triggerPointUngarrisonRight, "landPointRef": triggerPointLandPatrolRight }); // Order those ships to move to a randomly chosen trigger point on the determined // side of the river. Remember that chosen ungarrison point and the name of the // trigger points where the ungarrisoned units should patrol afterwards. for (let side of sides) for (let ship of side.ships) { let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef)); let ungarrisonPos = TriggerHelper.GetEntityPosition2D(ungarrisonPoint); this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef + " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")"); Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false); this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint }; } this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); }; /** * Check frequently whether the ships are close enough to unload at the shoreline. */ Trigger.prototype.CheckShipRange = function() { for (let ship of this.ships) { if (!this.shipTarget[ship] || PositionHelper.DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance) continue; let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); if (!cmpGarrisonHolder) continue; let humans = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Human"); let siegeEngines = TriggerHelper.MatchEntitiesByClass(cmpGarrisonHolder.GetEntities(), "Siege"); this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship])); cmpGarrisonHolder.UnloadAll(); this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships", true); if (randBool(formationProbability)) TriggerHelper.SetUnitFormation(gaulPlayer, humans, pickRandom(unitFormations)); this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege Engines", true); // Order soldiers at last, so the follow-player observer feature focuses the soldiers this.AttackAndPatrol(humans, unitTargetClass, this.shipTarget[ship].landPointRef, "Units", true); delete this.shipTarget[ship]; } }; Trigger.prototype.DanubiusOwnershipChange = function(data) { if (data.from != 0) return; if (this.heroes.delete(data.entity)) this.debugLog("Hero " + data.entity + " died"); if (this.ships.delete(data.entity)) this.debugLog("Ship " + data.entity + " sunk"); if (this.civicCenters.delete(data.entity)) this.debugLog("Gaia civic center " + data.entity + " destroyed or captured"); this.ritualEnts.delete(data.entity); }; Trigger.prototype.InitDanubius = function() { // Set a custom animation of idle ritual units frequently this.ritualEnts = new Set(); // To prevent spawning more than the limits, track IDs of current entities this.ships = new Set(); this.heroes = new Set(); // Remember gaia CCs to spawn attackers from this.civicCenters = new Set(TriggerHelper.GetPlayerEntitiesByClass(gaulPlayer, "CivCentre")); // Depends on this.heroes Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true }); // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name this.shipTarget = {}; this.fillShipsTimer = undefined; // Be able to distinguish between the left and right riverside - // TODO: The Vector2D types don't survive deserialization, so use an object with x and y properties only! let mapSize = TriggerHelper.GetMapSizeTerrain(); - this.mapCenter = clone(new Vector2D(mapSize / 2, mapSize / 2)); + this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2); - this.riverDirection = clone(Vector2D.sub( + this.riverDirection = Vector2D.sub( TriggerHelper.GetEntityPosition2D(this.GetTriggerPoints(triggerPointRiverDirection)[0]), - this.mapCenter)); + this.mapCenter); this.StartCelticRitual(); this.GarrisonAllGallicBuildings(); this.SpawnInitialCCDefenders(); this.SpawnCCAttackers(); this.SpawnShips(); this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); this.DoRepeatedly(5 * 1000, "CheckShipRange", {}); }; { Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).RegisterTrigger("OnInitGame", "InitDanubius", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/simulation/components/Player.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 24461) +++ ps/trunk/binaries/data/mods/public/simulation/components/Player.js (revision 24462) @@ -1,970 +1,976 @@ function Player() {} Player.prototype.Schema = "" + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + Resources.BuildSchema("positiveDecimal") + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + ""; /** * Don't serialize diplomacyColor or displayDiplomacyColor since they're modified by the GUI. */ Player.prototype.Serialize = function() { let state = {}; for (let key in this) if (this.hasOwnProperty(key)) state[key] = this[key]; state.diplomacyColor = undefined; state.displayDiplomacyColor = false; return state; }; +Player.prototype.Deserialize = function(state) +{ + for (let prop in state) + this[prop] = state[prop]; +}; + /** * Which units will be shown with special icons at the top. */ var panelEntityClasses = "Hero Relic"; Player.prototype.Init = function() { this.playerID = undefined; this.name = undefined; // Define defaults elsewhere (supporting other languages). this.civ = undefined; this.color = undefined; this.diplomacyColor = undefined; this.displayDiplomacyColor = false; this.popUsed = 0; // Population of units owned or trained by this player. this.popBonuses = 0; // Sum of population bonuses of player's entities. this.maxPop = 300; // Maximum population. this.trainingBlocked = false; // Indicates whether any training queue is currently blocked. this.resourceCount = {}; this.resourceGatherers = {}; this.tradingGoods = []; // Goods for next trade-route and its probabilities * 100. this.team = -1; // Team number of the player, players on the same team will always have ally diplomatic status. Also this is useful for team emblems, scoring, etc. this.teamsLocked = false; this.state = "active"; // Game state. One of "active", "defeated", "won". this.diplomacy = []; // Array of diplomatic stances for this player with respect to other players (including gaia and self). this.sharedDropsites = false; this.formations = []; this.startCam = undefined; this.controlAllUnits = false; this.isAI = false; this.cheatsEnabled = false; this.panelEntities = []; this.resourceNames = {}; this.disabledTemplates = {}; this.disabledTechnologies = {}; this.startingTechnologies = []; this.spyCostMultiplier = +this.template.SpyCostMultiplier; this.barterEntities = []; this.barterMultiplier = { "buy": clone(this.template.BarterMultiplier.Buy), "sell": clone(this.template.BarterMultiplier.Sell) }; // Initial resources. let resCodes = Resources.GetCodes(); for (let res of resCodes) { this.resourceCount[res] = 300; this.resourceNames[res] = Resources.GetResource(res).name; this.resourceGatherers[res] = 0; } // Trading goods probability in steps of 5. let resTradeCodes = Resources.GetTradableCodes(); let quotient = Math.floor(20 / resTradeCodes.length); let remainder = 20 % resTradeCodes.length; for (let i in resTradeCodes) this.tradingGoods.push({ "goods": resTradeCodes[i], "proba": 5 * (quotient + (+i < remainder ? 1 : 0)) }); }; Player.prototype.SetPlayerID = function(id) { this.playerID = id; }; Player.prototype.GetPlayerID = function() { return this.playerID; }; Player.prototype.SetName = function(name) { this.name = name; }; Player.prototype.GetName = function() { return this.name; }; Player.prototype.SetCiv = function(civcode) { let oldCiv = this.civ; this.civ = civcode; // Normally, the civ is only set once. But in Atlas, map designers can change civs at any time. if (oldCiv && this.playerID && oldCiv != civcode) Engine.BroadcastMessage(MT_CivChanged, { "player": this.playerID, "from": oldCiv, "to": civcode }); }; Player.prototype.GetCiv = function() { return this.civ; }; Player.prototype.SetColor = function(r, g, b) { let colorInitialized = !!this.color; this.color = { "r": r / 255, "g": g / 255, "b": b / 255, "a": 1 }; // Used in Atlas. if (colorInitialized) Engine.BroadcastMessage(MT_PlayerColorChanged, { "player": this.playerID }); }; Player.prototype.SetDiplomacyColor = function(color) { this.diplomacyColor = { "r": color.r / 255, "g": color.g / 255, "b": color.b / 255, "a": 1 }; }; Player.prototype.SetDisplayDiplomacyColor = function(displayDiplomacyColor) { this.displayDiplomacyColor = displayDiplomacyColor; }; Player.prototype.GetColor = function() { return this.color; }; Player.prototype.GetDisplayedColor = function() { return this.displayDiplomacyColor ? this.diplomacyColor : this.color; }; // Try reserving num population slots. Returns 0 on success or number of missing slots otherwise. Player.prototype.TryReservePopulationSlots = function(num) { if (num != 0 && num > (this.GetPopulationLimit() - this.popUsed)) return num - (this.GetPopulationLimit() - this.popUsed); this.popUsed += num; return 0; }; Player.prototype.UnReservePopulationSlots = function(num) { this.popUsed -= num; }; Player.prototype.GetPopulationCount = function() { return this.popUsed; }; Player.prototype.AddPopulation = function(num) { this.popUsed += num; }; Player.prototype.SetPopulationBonuses = function(num) { this.popBonuses = num; }; Player.prototype.AddPopulationBonuses = function(num) { this.popBonuses += num; }; Player.prototype.GetPopulationLimit = function() { return Math.min(this.GetMaxPopulation(), this.popBonuses); }; Player.prototype.SetMaxPopulation = function(max) { this.maxPop = max; }; Player.prototype.GetMaxPopulation = function() { return Math.round(ApplyValueModificationsToEntity("Player/MaxPopulation", this.maxPop, this.entity)); }; Player.prototype.CanBarter = function() { return this.barterEntities.length > 0; }; Player.prototype.GetBarterMultiplier = function() { return this.barterMultiplier; }; Player.prototype.GetSpyCostMultiplier = function() { return this.spyCostMultiplier; }; Player.prototype.GetPanelEntities = function() { return this.panelEntities; }; Player.prototype.IsTrainingBlocked = function() { return this.trainingBlocked; }; Player.prototype.BlockTraining = function() { this.trainingBlocked = true; }; Player.prototype.UnBlockTraining = function() { this.trainingBlocked = false; }; Player.prototype.SetResourceCounts = function(resources) { for (let res in resources) this.resourceCount[res] = resources[res]; }; Player.prototype.GetResourceCounts = function() { return this.resourceCount; }; Player.prototype.GetResourceGatherers = function() { return this.resourceGatherers; }; /** * @param {string} type - The generic type of resource to add the gatherer for. */ Player.prototype.AddResourceGatherer = function(type) { ++this.resourceGatherers[type]; }; /** * @param {string} type - The generic type of resource to remove the gatherer from. */ Player.prototype.RemoveResourceGatherer = function(type) { --this.resourceGatherers[type]; }; /** * Add resource of specified type to player. * @param {string} type - Generic type of resource. * @param {number} amount - Amount of resource, which should be added. */ Player.prototype.AddResource = function(type, amount) { this.resourceCount[type] += +amount; }; /** * Add resources to player. */ Player.prototype.AddResources = function(amounts) { for (let type in amounts) this.resourceCount[type] += +amounts[type]; }; Player.prototype.GetNeededResources = function(amounts) { // Check if we can afford it all. let amountsNeeded = {}; for (let type in amounts) if (this.resourceCount[type] != undefined && amounts[type] > this.resourceCount[type]) amountsNeeded[type] = amounts[type] - Math.floor(this.resourceCount[type]); if (Object.keys(amountsNeeded).length == 0) return undefined; return amountsNeeded; }; Player.prototype.SubtractResourcesOrNotify = function(amounts) { let amountsNeeded = this.GetNeededResources(amounts); // If we don't have enough resources, send a notification to the player. if (amountsNeeded) { let parameters = {}; let i = 0; for (let type in amountsNeeded) { ++i; parameters["resourceType" + i] = this.resourceNames[type]; parameters["resourceAmount" + i] = amountsNeeded[type]; } let msg = ""; // When marking strings for translations, you need to include the actual string, // not some way to derive the string. if (i < 1) warn("Amounts needed but no amounts given?"); else if (i == 1) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s"); else if (i == 2) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s"); else if (i == 3) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s"); else if (i == 4) msg = markForTranslation("Insufficient resources - %(resourceAmount1)s %(resourceType1)s, %(resourceAmount2)s %(resourceType2)s, %(resourceAmount3)s %(resourceType3)s, %(resourceAmount4)s %(resourceType4)s"); else warn("Localisation: Strings are not localised for more than 4 resources"); // Send as time-notification. let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "players": [this.playerID], "message": msg, "parameters": parameters, "translateMessage": true, "translateParameters": { "resourceType1": "withinSentence", "resourceType2": "withinSentence", "resourceType3": "withinSentence", "resourceType4": "withinSentence" } }); return false; } for (let type in amounts) this.resourceCount[type] -= amounts[type]; return true; }; Player.prototype.TrySubtractResources = function(amounts) { if (!this.SubtractResourcesOrNotify(amounts)) return false; let cmpStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpStatisticsTracker) for (let type in amounts) cmpStatisticsTracker.IncreaseResourceUsedCounter(type, amounts[type]); return true; }; Player.prototype.GetNextTradingGoods = function() { let value = randFloat(0, 100); let last = this.tradingGoods.length - 1; let sumProba = 0; for (let i = 0; i < last; ++i) { sumProba += this.tradingGoods[i].proba; if (value < sumProba) return this.tradingGoods[i].goods; } return this.tradingGoods[last].goods; }; Player.prototype.GetTradingGoods = function() { let tradingGoods = {}; for (let resource of this.tradingGoods) tradingGoods[resource.goods] = resource.proba; return tradingGoods; }; Player.prototype.SetTradingGoods = function(tradingGoods) { let resTradeCodes = Resources.GetTradableCodes(); let sumProba = 0; for (let resource in tradingGoods) { if (resTradeCodes.indexOf(resource) == -1 || tradingGoods[resource] < 0) { error("Invalid trading goods: " + uneval(tradingGoods)); return; } sumProba += tradingGoods[resource]; } if (sumProba != 100) { error("Invalid trading goods probability: " + uneval(sumProba)); return; } this.tradingGoods = []; for (let resource in tradingGoods) this.tradingGoods.push({ "goods": resource, "proba": tradingGoods[resource] }); }; Player.prototype.GetState = function() { return this.state; }; /** * @param {string} newState - Either "defeated" or "won". * @param {string|undefined} message - A string to be shown in chat, for example * markForTranslation("%(player)s has been defeated (failed objective)."). * If it is undefined, the caller MUST send that GUI notification manually. */ Player.prototype.SetState = function(newState, message) { if (this.state != "active") return; if (newState != "won" && newState != "defeated") { warn("Can't change playerstate to " + this.state); return; } if (!this.playerID) { warn("Gaia can't change state."); return; } this.state = newState; let won = newState == "won"; let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (won) cmpRangeManager.SetLosRevealAll(this.playerID, true); else { // Reassign all player's entities to Gaia. let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); // The ownership change is done in two steps so that entities don't hit idle // (and thus possibly look for "enemies" to attack) before nearby allies get // converted to Gaia as well. for (let entity of entities) { let cmpOwnership = Engine.QueryInterface(entity, IID_Ownership); cmpOwnership.SetOwnerQuiet(0); } // With the real ownership change complete, send OwnershipChanged messages. for (let entity of entities) Engine.PostMessage(entity, MT_OwnershipChanged, { "entity": entity, "from": this.playerID, "to": 0 }); } Engine.PostMessage(this.entity, won ? MT_PlayerWon : MT_PlayerDefeated, { "playerId": this.playerID }); if (message) { let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.PushNotification({ "type": won ? "won" : "defeat", "players": [this.playerID], "allies": [this.playerID], "message": message }); } }; Player.prototype.GetTeam = function() { return this.team; }; Player.prototype.SetTeam = function(team) { if (this.teamsLocked) return; this.team = team; // Set all team members as allies. if (this.team != -1) { let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let cmpPlayer = QueryPlayerIDInterface(i); if (this.team != cmpPlayer.GetTeam()) continue; this.SetAlly(i); cmpPlayer.SetAlly(this.playerID); } } Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetLockTeams = function(value) { this.teamsLocked = value; }; Player.prototype.GetLockTeams = function() { return this.teamsLocked; }; Player.prototype.GetDiplomacy = function() { return this.diplomacy.slice(); }; Player.prototype.SetDiplomacy = function(dipl) { this.diplomacy = dipl.slice(); Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": null }); }; Player.prototype.SetDiplomacyIndex = function(idx, value) { let cmpPlayer = QueryPlayerIDInterface(idx); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; this.diplomacy[idx] = value; Engine.BroadcastMessage(MT_DiplomacyChanged, { "player": this.playerID, "otherPlayer": cmpPlayer.GetPlayerID() }); // Mutual worsening of relations. if (cmpPlayer.diplomacy[this.playerID] > value) cmpPlayer.SetDiplomacyIndex(this.playerID, value); }; Player.prototype.UpdateSharedLos = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (!cmpRangeManager || !cmpTechnologyManager) return; if (!cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech)) { cmpRangeManager.SetSharedLos(this.playerID, [this.playerID]); return; } cmpRangeManager.SetSharedLos(this.playerID, this.GetMutualAllies()); }; Player.prototype.GetFormations = function() { return this.formations; }; Player.prototype.SetFormations = function(formations) { this.formations = formations; }; Player.prototype.GetStartingCameraPos = function() { return this.startCam.position; }; Player.prototype.GetStartingCameraRot = function() { return this.startCam.rotation; }; Player.prototype.SetStartingCamera = function(pos, rot) { this.startCam = { "position": pos, "rotation": rot }; }; Player.prototype.HasStartingCamera = function() { return this.startCam !== undefined; }; Player.prototype.HasSharedLos = function() { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched(this.template.SharedLosTech); }; Player.prototype.HasSharedDropsites = function() { return this.sharedDropsites; }; Player.prototype.SetControlAllUnits = function(c) { this.controlAllUnits = c; }; Player.prototype.CanControlAllUnits = function() { return this.controlAllUnits; }; Player.prototype.SetAI = function(flag) { this.isAI = flag; }; Player.prototype.IsAI = function() { return this.isAI; }; Player.prototype.GetPlayersByDiplomacy = function(func) { let players = []; for (let i = 0; i < this.diplomacy.length; ++i) if (this[func](i)) players.push(i); return players; }; Player.prototype.SetAlly = function(id) { this.SetDiplomacyIndex(id, 1); }; /** * Check if given player is our ally. */ Player.prototype.IsAlly = function(id) { return this.diplomacy[id] > 0; }; Player.prototype.GetAllies = function() { return this.GetPlayersByDiplomacy("IsAlly"); }; /** * Check if given player is our ally excluding ourself */ Player.prototype.IsExclusiveAlly = function(id) { return this.playerID != id && this.IsAlly(id); }; /** * Check if given player is our ally, and we are its ally */ Player.prototype.IsMutualAlly = function(id) { let cmpPlayer = QueryPlayerIDInterface(id); return this.IsAlly(id) && cmpPlayer && cmpPlayer.IsAlly(this.playerID); }; Player.prototype.GetMutualAllies = function() { return this.GetPlayersByDiplomacy("IsMutualAlly"); }; /** * Check if given player is our ally, and we are its ally, excluding ourself */ Player.prototype.IsExclusiveMutualAlly = function(id) { return this.playerID != id && this.IsMutualAlly(id); }; Player.prototype.SetEnemy = function(id) { this.SetDiplomacyIndex(id, -1); }; /** * Check if given player is our enemy */ Player.prototype.IsEnemy = function(id) { return this.diplomacy[id] < 0; }; Player.prototype.GetEnemies = function() { return this.GetPlayersByDiplomacy("IsEnemy"); }; Player.prototype.SetNeutral = function(id) { this.SetDiplomacyIndex(id, 0); }; /** * Check if given player is neutral */ Player.prototype.IsNeutral = function(id) { return this.diplomacy[id] == 0; }; /** * Do some map dependant initializations */ Player.prototype.OnGlobalInitGame = function(msg) { let cmpTechnologyManager = Engine.QueryInterface(this.entity, IID_TechnologyManager); if (cmpTechnologyManager) for (let tech of this.startingTechnologies) cmpTechnologyManager.ResearchTechnology(tech); // Replace the "{civ}" code with this civ ID. let disabledTemplates = this.disabledTemplates; this.disabledTemplates = {}; for (let template in disabledTemplates) if (disabledTemplates[template]) this.disabledTemplates[template.replace(/\{civ\}/g, this.civ)] = true; }; /** * Keep track of population effects of all entities that * become owned or unowned by this player. */ Player.prototype.OnGlobalOwnershipChanged = function(msg) { if (msg.from != this.playerID && msg.to != this.playerID) return; let cmpCost = Engine.QueryInterface(msg.entity, IID_Cost); if (msg.from == this.playerID) { if (cmpCost) this.popUsed -= cmpCost.GetPopCost(); let panelIndex = this.panelEntities.indexOf(msg.entity); if (panelIndex >= 0) this.panelEntities.splice(panelIndex, 1); let barterIndex = this.barterEntities.indexOf(msg.entity); if (barterIndex >= 0) this.barterEntities.splice(barterIndex, 1); } if (msg.to == this.playerID) { if (cmpCost) this.popUsed += cmpCost.GetPopCost(); let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); if (!cmpIdentity) return; if (MatchesClassList(cmpIdentity.GetClassesList(), panelEntityClasses)) this.panelEntities.push(msg.entity); if (cmpIdentity.HasClass("Barter") && !Engine.QueryInterface(msg.entity, IID_Foundation)) this.barterEntities.push(msg.entity); } }; Player.prototype.OnResearchFinished = function(msg) { if (msg.tech == this.template.SharedLosTech) this.UpdateSharedLos(); else if (msg.tech == this.template.SharedDropsitesTech) this.sharedDropsites = true; }; Player.prototype.OnDiplomacyChanged = function() { this.UpdateSharedLos(); }; Player.prototype.OnValueModification = function(msg) { if (msg.component != "Player") return; if (msg.valueNames.indexOf("Player/SpyCostMultiplier") != -1) this.spyCostMultiplier = ApplyValueModificationsToEntity("Player/SpyCostMultiplier", +this.template.SpyCostMultiplier, this.entity); if (msg.valueNames.some(mod => mod.startsWith("Player/BarterMultiplier/"))) for (let res in this.template.BarterMultiplier.Buy) { this.barterMultiplier.buy[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Buy/"+res, +this.template.BarterMultiplier.Buy[res], this.entity); this.barterMultiplier.sell[res] = ApplyValueModificationsToEntity("Player/BarterMultiplier/Sell/"+res, +this.template.BarterMultiplier.Sell[res], this.entity); } }; Player.prototype.SetCheatsEnabled = function(flag) { this.cheatsEnabled = flag; }; Player.prototype.GetCheatsEnabled = function() { return this.cheatsEnabled; }; Player.prototype.TributeResource = function(player, amounts) { let cmpPlayer = QueryPlayerIDInterface(player); if (!cmpPlayer) return; if (this.state != "active" || cmpPlayer.state != "active") return; let resTribCodes = Resources.GetTributableCodes(); for (let resCode in amounts) if (resTribCodes.indexOf(resCode) == -1 || !Number.isInteger(amounts[resCode]) || amounts[resCode] < 0) { warn("Invalid tribute amounts: " + uneval(resCode) + ": " + uneval(amounts)); return; } if (!this.SubtractResourcesOrNotify(amounts)) return; cmpPlayer.AddResources(amounts); let total = Object.keys(amounts).reduce((sum, type) => sum + amounts[type], 0); let cmpOurStatisticsTracker = QueryPlayerIDInterface(this.playerID, IID_StatisticsTracker); if (cmpOurStatisticsTracker) cmpOurStatisticsTracker.IncreaseTributesSentCounter(total); let cmpTheirStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker); if (cmpTheirStatisticsTracker) cmpTheirStatisticsTracker.IncreaseTributesReceivedCounter(total); let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); if (cmpGUIInterface) cmpGUIInterface.PushNotification({ "type": "tribute", "players": [player], "donator": this.playerID, "amounts": amounts }); Engine.BroadcastMessage(MT_TributeExchanged, { "to": player, "from": this.playerID, "amounts": amounts }); }; Player.prototype.AddDisabledTemplate = function(template) { this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTemplate = function(template) { this.disabledTemplates[template] = false; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTemplates = function(templates) { this.disabledTemplates = {}; for (let template of templates) this.disabledTemplates[template] = true; Engine.BroadcastMessage(MT_DisabledTemplatesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTemplates = function() { return this.disabledTemplates; }; Player.prototype.AddDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.RemoveDisabledTechnology = function(tech) { this.disabledTechnologies[tech] = false; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.SetDisabledTechnologies = function(techs) { this.disabledTechnologies = {}; for (let tech of techs) this.disabledTechnologies[tech] = true; Engine.BroadcastMessage(MT_DisabledTechnologiesChanged, { "player": this.playerID }); }; Player.prototype.GetDisabledTechnologies = function() { return this.disabledTechnologies; }; Player.prototype.AddStartingTechnology = function(tech) { if (this.startingTechnologies.indexOf(tech) == -1) this.startingTechnologies.push(tech); }; Player.prototype.SetStartingTechnologies = function(techs) { this.startingTechnologies = techs; }; Player.prototype.OnGlobalPlayerDefeated = function(msg) { let cmpSound = Engine.QueryInterface(this.entity, IID_Sound); if (!cmpSound) return; let soundGroup = cmpSound.GetSoundGroup(this.playerID === msg.playerId ? "defeated" : this.IsAlly(msg.playerId) ? "defeated_ally" : this.state === "won" ? "won" : "defeated_enemy"); if (soundGroup) Engine.QueryInterface(SYSTEM_ENTITY, IID_SoundManager).PlaySoundGroupForPlayer(soundGroup, this.playerID); }; Engine.RegisterComponentType(IID_Player, "Player", Player); Index: ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 24461) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/Setup.js (revision 24462) @@ -1,72 +1,72 @@ /** * Used to initialize non-player settings relevant to the map, like * default stance and victory conditions. DO NOT load players here */ function LoadMapSettings(settings) { if (!settings) settings = {}; if (settings.DefaultStance) for (let ent of Engine.GetEntitiesWithInterface(IID_UnitAI)) { let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); cmpUnitAI.SwitchToStance(settings.DefaultStance); } if (settings.RevealMap) { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetLosRevealAll(-1, true); } if (settings.DisableTreasures) for (let ent of Engine.GetEntitiesWithInterface(IID_ResourceSupply)) { let cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); if (cmpResourceSupply.GetType().generic == "treasure") Engine.DestroyEntity(ent); } let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) cmpRangeManager.SetLosCircular(!!settings.CircularMap); let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); if (cmpObstructionManager) cmpObstructionManager.SetPassabilityCircular(!!settings.CircularMap); if (settings.TriggerDifficulty !== undefined) Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(settings.TriggerDifficulty); else if (settings.SupportedTriggerDifficulties) // used by Atlas and autostart games { let difficulties = Engine.ReadJSONFile("simulation/data/settings/trigger_difficulties.json").Data; let defaultDiff = difficulties.find(d => d.Name == settings.SupportedTriggerDifficulties.Default).Difficulty; Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger).SetDifficulty(defaultDiff); } let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); - let gameSettings = { "victoryConditions": settings.VictoryConditions }; + let gameSettings = { "victoryConditions": clone(settings.VictoryConditions) }; if (gameSettings.victoryConditions.indexOf("capture_the_relic") != -1) { gameSettings.relicCount = settings.RelicCount; gameSettings.relicDuration = settings.RelicDuration * 60 * 1000; } if (gameSettings.victoryConditions.indexOf("wonder") != -1) gameSettings.wonderDuration = settings.WonderDuration * 60 * 1000; if (gameSettings.victoryConditions.indexOf("regicide") != -1) gameSettings.regicideGarrison = settings.RegicideGarrison; cmpEndGameManager.SetGameSettings(gameSettings); cmpEndGameManager.SetAlliedVictory(settings.LockTeams || !settings.LastManStanding); if (settings.LockTeams && settings.LastManStanding) warn("Last man standing is only available in games with unlocked teams!"); let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager); if (settings.Ceasefire) cmpCeasefireManager.StartCeasefire(settings.Ceasefire * 60 * 1000); } Engine.RegisterGlobal("LoadMapSettings", LoadMapSettings); Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24461) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24462) @@ -1,1051 +1,1056 @@ /* Copyright (C) 2020 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 "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @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 shared_ptr& context); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. 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 }; ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx) { m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_formerRealm); } 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(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::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(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::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(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::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(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(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(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ JS::RootedValue ret(cx); if (!JS_StructuredClone(rq.cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() != 1 || !args.get(0).isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); return false; } ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); args.rval().set(args.get(0)); return true; } bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); args.rval().setUndefined(); return true; } bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) { const char* name = "(ProfileAttribute)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } g_Profiler2.RecordAttribute("%s", name); args.rval().setUndefined(); return true; } // 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; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : 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); 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); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); 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); JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); Register("ProfileAttribute", ::ProfileAttribute, 1); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const { JSAutoRealm autoRealm(m_cx, m_glob); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(new ScriptInterface_impl(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } 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; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const { m->Register(name, fptr, (uint)nargs); } JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } 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 ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } 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::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; if (JS_CallFunctionName(rq.cx, obj, name, argv, ret)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS::NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } 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::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_DefineProperty(rq.cx, object, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); return JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs); } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } -bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { - return GetProperty_(obj, name, out); + ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); } -bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { - ScriptRequest rq(this); JS::RootedValue val(rq.cx); - if (!GetProperty_(obj, name, &val)) + if (!GetProperty(rq, obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } -bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { - return GetPropertyInt_(obj, name, out); + ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); } -bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { - ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetProperty(rq.cx, object, name, out); } -bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const +bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); + return GetPropertyInt(rq,obj, name, out); +} + +bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out) +{ JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetPropertyById(rq.cx, object, nameId, out); } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } 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::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { ScriptRequest rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } 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::FreezeObject(JS::HandleValue objVal, bool deep) const { ScriptRequest rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } 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; } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { ScriptRequest rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; ScriptException::CatchPending(rq); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } 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 content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { ScriptRequest rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { ScriptException::CatchPending(rq); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { ScriptRequest rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); if (JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) return str.stream.str(); // Drop exceptions raised by cyclic values before trying something else JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); ScriptRequest rq(this); JS::RootedValue out(rq.cx); ScriptInterface::StructuredClone structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { ScriptRequest rq(this); ScriptInterface::StructuredClone ret(new JSStructuredCloneData(JS::StructuredCloneScope::SameProcess)); JS::CloneDataPolicy policy; if (!JS_WriteStructuredClone(rq.cx, v, ret.get(), JS::StructuredCloneScope::SameProcess, policy, nullptr, nullptr, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); ScriptException::CatchPending(rq); return ScriptInterface::StructuredClone(); } return ret; } void ScriptInterface::ReadStructuredClone(const ScriptInterface::StructuredClone& ptr, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::CloneDataPolicy policy; if (!JS_ReadStructuredClone(rq.cx, *ptr, JS_STRUCTURED_CLONE_VERSION, ptr->scope(), ret, policy, nullptr, nullptr)) ScriptException::CatchPending(rq); } Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24461) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24462) @@ -1,619 +1,632 @@ /* Copyright (C) 2020 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 "lib/file/vfs/vfs_path.h" #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptExceptions.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, RegisterFunctionFailed); 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 JSStructuredCloneData; 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 shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } /** * RAII structure which encapsulates an access to the context and compartment of a ScriptInterface. * This struct provides: * - a pointer to the context, while acting like JSAutoRequest * - a pointer to the global object of the compartment, while acting like JSAutoRealm * * This way, getting and using those pointers is safe with respect to the GC * and to the separation of compartments. */ class ScriptRequest { public: ScriptRequest() = delete; ScriptRequest(const ScriptRequest& rq) = delete; ScriptRequest& operator=(const ScriptRequest& rq) = delete; ScriptRequest(const ScriptInterface& scriptInterface); ScriptRequest(const ScriptInterface* scriptInterface) : ScriptRequest(*scriptInterface) {} ScriptRequest(shared_ptr scriptInterface) : ScriptRequest(*scriptInterface) {} ~ScriptRequest(); JS::Value globalValue() const; JSContext* cx; JSObject* glob; private: JS::Realm* m_formerRealm; }; /** * 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 RegisterFunction) 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 shared_ptr& context); ~ScriptInterface(); struct CmptPrivate { ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions } m_CmptPrivate; void SetCallbackData(void* pCBData); static CmptPrivate* GetScriptInterfaceAndCBData(JSContext* cx); /** * 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; 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 geenrator with a seeded, network-sync'd 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); /** * Sets the given value to a new plain JS::Object, converts the arguments to JS::Values and sets them as properties. * This is static so that callers like ToJSVal can use it with the JSContext directly instead of having to obtain the instance using GetScriptInterfaceAndCBData. * Can throw an exception. */ template static bool CreateObject(const ScriptRequest& rq, JS::MutableHandleValue objectValue, Args const&... args) { JS::RootedObject obj(rq.cx); if (!CreateObject_(rq, &obj, args...)) return false; objectValue.setObject(*obj); return true; } /** * Sets the given value to a new JS object or Null Value in case of out-of-memory. */ static void CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length = 0); /** * 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); /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the integer-named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant = false, bool enumerate = true) const; /** * Get the named property on the given object. */ template bool GetProperty(JS::HandleValue obj, const char* name, T& out) const; - - /** - * Get the named property of the given object. - */ bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const; + template + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out); + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out); + static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out); + /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const; - - /** - * Get the named property of the given object. - */ bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const; + bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleObject out) const; + + template + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out); + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out); + static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleObject out); /** * Check the named property has been defined on the given object. */ bool HasProperty(JS::HandleValue obj, const char* name) const; /** * 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); /** * Returns all properties of the object, both own properties and inherited. * This is essentially equivalent to calling Object.getOwnPropertyNames() * and recursing up the prototype chain. * NB: this does not return properties with symbol or numeric keys, as that would * require a variant in the vector, and it's not useful for now. * @param enumerableOnly - only return enumerable properties. */ bool EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const; bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); bool FreezeObject(JS::HandleValue objVal, bool deep) const; /** * Convert an object to a UTF-8 encoded string, either with JSON * (if pretty == true and there is no JSON error) or with toSource(). * * We have to use a mutable handle because JS_Stringify requires that for unknown reasons. */ std::string ToString(JS::MutableHandleValue obj, bool pretty = false) const; /** * Parse a UTF-8-encoded JSON string. Returns the unmodified value on error * and prints an error message. * @return true on success; false otherwise */ bool ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const; /** * Read a JSON file. Returns the unmodified value on error and prints an error message. */ void ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const; /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ std::string StringifyJSON(JS::MutableHandleValue obj, bool indent = true) const; /** * 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; /** * Convert a JS::Value to a C++ type. (This might trigger GC.) */ template static bool FromJSVal(const ScriptRequest& rq, const JS::HandleValue val, T& ret); /** * Convert a C++ type to a JS::Value. (This might trigger GC. The return * value must be rooted if you don't want it to be collected.) * NOTE: We are passing the JS::Value by reference instead of returning it by value. * The reason is a memory corruption problem that appears to be caused by a bug in Visual Studio. * Details here: http://www.wildfiregames.com/forum/index.php?showtopic=17289&p=285921 */ template static void ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, T const& val); /** * Convert a named property of an object to a C++ type. */ template static bool FromJSProperty(const ScriptRequest& rq, const JS::HandleValue val, const char* name, T& ret, bool strict = false); /** * MathRandom (this function) calls the random number generator assigned to this ScriptInterface instance and * returns the generated number. * Math_random (with underscore, not this function) is a global function, but different random number generators can be * stored per ScriptInterface. It calls MathRandom of the current ScriptInterface instance. */ bool MathRandom(double& nbr); /** * Structured clones are a way to serialize 'simple' JS::Values into a buffer * that can safely be passed between compartments and between threads. * A StructuredClone can be stored and read multiple times if desired. * We wrap them in shared_ptr so memory management is automatic and * thread-safe. */ using StructuredClone = shared_ptr; StructuredClone WriteStructuredClone(JS::HandleValue v) const; void ReadStructuredClone(const StructuredClone& ptr, JS::MutableHandleValue ret) const; /** * Construct a new value (usable in this ScriptInterface's compartment) by cloning * a value from a different compartment. * Complex values (functions, XML, etc) won't be cloned correctly, but basic * types and cyclic references should be fine. */ JS::Value CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const; /** * 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; } /** * Converts |a| if needed and assigns it to |handle|. * This is meant for use in other templates where we want to use the same code for JS::RootedValue&/JS::HandleValue and * other types. Note that functions are meant to take JS::HandleValue instead of JS::RootedValue&, but this implicit * conversion does not work for templates (exact type matches required for type deduction). * A similar functionality could also be implemented as a ToJSVal specialization. The current approach was preferred * because "conversions" from JS::HandleValue to JS::MutableHandleValue are unusual and should not happen "by accident". */ template static void AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a); /** * The same as AssignOrToJSVal, but also allows JS::Value for T. * In most cases it's not safe to use the plain (unrooted) JS::Value type, but this can happen quite * easily with template functions. The idea is that the linker prints an error if AssignOrToJSVal is * used with JS::Value. If the specialization for JS::Value should be allowed, you can use this * "unrooted" version of AssignOrToJSVal. */ template static void AssignOrToJSValUnrooted(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { AssignOrToJSVal(rq, handle, a); } /** * Converts |val| to T if needed or just returns it if it's a handle. * This is meant for use in other templates where we want to use the same code for JS::HandleValue and * other types. */ template static T AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret); private: static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj); template static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj, const char* propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, propertyValue); return CreateObject_(rq, obj, args...) && JS_DefineProperty(rq.cx, obj, propertyName, val, JSPROP_ENUMERATE); } bool CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const; bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const; - bool GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; - bool GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue value) const; struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; void Register(const char* name, JSNative fptr, size_t nargs) const; // 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; boost::random::rand48* m_rng; std::map m_CustomObjectTypes; // The nasty macro/template bits are split into a separate file so you don't have to look at them public: #include "NativeWrapperDecls.h" // This declares: // // template // void RegisterFunction(const char* functionName) const; // // template // static JSNative call; // // template // static JSNative callMethod; // // template // static JSNative callMethodConst; // // template // static size_t nargs(); // // template // bool CallFunction(JS::HandleValue val, const char* name, R& ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const T0&...) const; // // template // bool CallFunctionVoid(JS::HandleValue val, const char* name, const T0&...) const; }; // Implement those declared functions #include "NativeWrapperDefns.h" template inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { ToJSVal(rq, handle, a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::PersistentRootedValue& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal >(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Heap& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::RootedValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::HandleValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSValUnrooted(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Value& a) { handle.set(a); } template inline T ScriptInterface::AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret) { T retVal; ret = FromJSVal(rq, val, retVal); return retVal; } template<> inline JS::HandleValue ScriptInterface::AssignOrFromJSVal(const ScriptRequest& UNUSED(rq), const JS::HandleValue& val, bool& ret) { ret = true; return val; } template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetPropertyInt_(obj, name, val, constant, enumerate); } template bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const { ScriptRequest rq(this); + return GetProperty(rq, obj, name, out); +} + +template +bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out) +{ JS::RootedValue val(rq.cx); - if (!GetProperty_(obj, name, &val)) + if (!GetProperty(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const { ScriptRequest rq(this); + return GetPropertyInt(rq, obj, name, out); +} + +template +bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out) +{ JS::RootedValue val(rq.cx); - if (!GetPropertyInt_(obj, name, &val)) + if (!GetPropertyInt(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } 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 FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24461) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24462) @@ -1,1159 +1,1122 @@ /* Copyright (C) 2020 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/ScriptContext.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). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->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 (!m_ScriptInterface->HasProperty(metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName); if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName); return false; } if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor)) { LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8()); return false; } // Get the constructor function from the loaded scripts if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(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(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector commands; }; CAIWorker() : m_ScriptInterface(new ScriptInterface("Engine", "AI", g_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_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); m_ScriptInterface->RegisterFunction("PostCommand"); m_ScriptInterface->RegisterFunction("IncludeModule"); m_ScriptInterface->RegisterFunction("Exit"); m_ScriptInterface->RegisterFunction("ComputePath"); m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); m_ScriptInterface->RegisterFunction("GetTemplate"); JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get())); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->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; } static void IncludeModule(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); self->LoadScripts(name); } static void PostCommand(ScriptInterface::CmptPrivate* pCmptPrivate, int playerid, JS::HandleValue cmd) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); self->PostCommand(playerid, cmd); } void PostCommand(int playerid, JS::HandleValue cmd) { for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } static JS::Value ComputePath(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); ScriptRequest rq(self->m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); self->m_ScriptInterface->FromJSVal(rq, position, pos); self->m_ScriptInterface->FromJSVal(rq, goal, goalPos); self->ComputePath(pos, goalPos, passClass, waypoints); self->m_ScriptInterface->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); } static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) { ENSURE(pCmptPrivate->pCBData); CAIWorker* self = static_cast (pCmptPrivate->pCBData); return self->GetTemplate(name); } CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } static void ExitProgram(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { QuitEngine(); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ static void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } void SetRNGSeed(u32 seed) { m_RNG.seed(seed); } bool TryLoadSharedComponent() { 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 (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue playersID(rq.cx); ScriptInterface::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); m_ScriptInterface->ToJSVal(rq, &val, m_Players[i]->m_Player); m_ScriptInterface->SetPropertyInt(playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); ScriptInterface::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) { shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, behavior, m_ScriptInterface)); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const ScriptInterface::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); m_ScriptInterface->ReadStructuredClone(gameState, &state); ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); ScriptInterface::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) { m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void UpdateGameState(const ScriptInterface::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) ScriptInterface::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) ScriptInterface::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; ScriptInterface::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); m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true); } } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); 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) - { - JS::RootedValue sharedData(rq.cx); - if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData)) - LOGERROR("AI shared script Serialize call failed"); - serializer.ScriptVal("sharedData", &sharedData); - } + 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); m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } - bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize"); - if (hasCustomSerialize) - { - JS::RootedValue scriptData(rq.cx); - if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData)) - LOGERROR("AI script Serialize call failed"); - serializer.ScriptVal("data", &scriptData); - } - else - { - serializer.ScriptVal("data", &m_Players[i]->m_Obj); - } + 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(); - JS::RootedValue sharedData(rq.cx); - deserializer.ScriptVal("sharedData", &sharedData); - if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData)) - LOGERROR("AI shared script Deserialize call failed"); + 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(m_ScriptInterface->WriteStructuredClone(val)); } - bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize"); - if (hasCustomDeserialize) - { - JS::RootedValue scriptData(rq.cx); - deserializer.ScriptVal("data", &scriptData); - if (m_Players[i]->m_UseSharedComponent) - { - if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj)) - LOGERROR("AI script Deserialize call failed"); - } - else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData)) - { - LOGERROR("AI script deserialize() call failed"); - } - } - else - { - deserializer.ScriptVal("data", &m_Players.back()->m_Obj); - } + 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 m_ScriptInterface->ReadJSONFile(path, out); m_PlayerMetadata[path] = JS::Heap(out); return; } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); m_ScriptInterface->ReadStructuredClone(m_GameState, &state); m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptContext; 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; ScriptInterface::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_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(scriptInterface.WriteStructuredClone(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(scriptInterface.WriteStructuredClone(state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check 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) { scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal); } } } private: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue classesVal(rq.cx); ScriptInterface::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true); scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24461) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24462) @@ -1,110 +1,131 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "simulation2/system/ComponentTest.h" +#include "simulation2/serialization/StdDeserializer.h" +#include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } + static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) + { + ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); + + JS::RootedValue val(rq.cx); + val = value; + std::stringstream stream; + CStdSerializer serializer(scriptInterface, stream); + serializer.ScriptVal("", &val); + CStdDeserializer deserializer(scriptInterface, stream); + deserializer.ScriptVal("", &val); + return val; + } + void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); + + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); + load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); + componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/scripting/ScriptComponent.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.cpp (revision 24461) +++ ps/trunk/source/simulation2/scripting/ScriptComponent.cpp (revision 24462) @@ -1,119 +1,70 @@ /* Copyright (C) 2020 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 "ScriptComponent.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/serialization/IDeserializer.h" CComponentTypeScript::CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance) : m_ScriptInterface(scriptInterface), m_Instance(scriptInterface.GetGeneralJSContext(), instance) { - // Cache the property detection for efficiency - ScriptRequest rq(m_ScriptInterface); - - m_HasCustomSerialize = m_ScriptInterface.HasProperty(m_Instance, "Serialize"); - m_HasCustomDeserialize = m_ScriptInterface.HasProperty(m_Instance, "Deserialize"); - - m_HasNullSerialize = false; - if (m_HasCustomSerialize) - { - JS::RootedValue val(rq.cx); - if (m_ScriptInterface.GetProperty(m_Instance, "Serialize", &val) && val.isNull()) - m_HasNullSerialize = true; - } } void CComponentTypeScript::Init(const CParamNode& paramNode, entity_id_t ent) { m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false); m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false); m_ScriptInterface.CallFunctionVoid(m_Instance, "Init"); } void CComponentTypeScript::Deinit() { m_ScriptInterface.CallFunctionVoid(m_Instance, "Deinit"); } void CComponentTypeScript::HandleMessage(const CMessage& msg, bool global) { ScriptRequest rq(m_ScriptInterface); const char* name = global ? msg.GetScriptGlobalHandlerName() : msg.GetScriptHandlerName(); JS::RootedValue msgVal(rq.cx, msg.ToJSValCached(m_ScriptInterface)); if (!m_ScriptInterface.CallFunctionVoid(m_Instance, name, msgVal)) LOGERROR("Script message handler %s failed", name); } void CComponentTypeScript::Serialize(ISerializer& serialize) { - // If the component set Serialize = null, then do no work here - if (m_HasNullSerialize) - return; - ScriptRequest rq(m_ScriptInterface); - // Support a custom "Serialize" function, which returns a new object that will be - // serialized instead of the component itself - if (m_HasCustomSerialize) - { - JS::RootedValue val(rq.cx); - if (!m_ScriptInterface.CallFunction(m_Instance, "Serialize", &val)) - LOGERROR("Script Serialize call failed"); - serialize.ScriptVal("object", &val); - } - else - { - serialize.ScriptVal("object", &m_Instance); - } + serialize.ScriptVal("comp", &m_Instance); } void CComponentTypeScript::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent) { ScriptRequest rq(m_ScriptInterface); + deserialize.ScriptObjectAssign("comp", m_Instance); + m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false); m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false); - // Support a custom "Deserialize" function, to which we pass the deserialized data - // instead of automatically adding the deserialized properties onto the object - if (m_HasCustomDeserialize) - { - JS::RootedValue val(rq.cx); - - // If Serialize = null, we'll still call Deserialize but with undefined argument - if (!m_HasNullSerialize) - deserialize.ScriptVal("object", &val); - - if (!m_ScriptInterface.CallFunctionVoid(m_Instance, "Deserialize", val)) - LOGERROR("Script Deserialize call failed"); - } - else - { - if (!m_HasNullSerialize) - { - // Use ScriptObjectAppend so we don't lose the carefully-constructed - // prototype/parent of this object - deserialize.ScriptObjectAppend("object", m_Instance); - } - } } Index: ps/trunk/source/simulation2/scripting/ScriptComponent.h =================================================================== --- ps/trunk/source/simulation2/scripting/ScriptComponent.h (revision 24461) +++ ps/trunk/source/simulation2/scripting/ScriptComponent.h (revision 24462) @@ -1,78 +1,75 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 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_SCRIPTCOMPONENT #define INCLUDED_SCRIPTCOMPONENT #include "simulation2/system/Component.h" #include "ps/CLogger.h" class CSimContext; class CParamNode; class ISerializer; class IDeserializer; class CComponentTypeScript { NONCOPYABLE(CComponentTypeScript); public: CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance); JS::Value GetInstance() const { return m_Instance.get(); } void Init(const CParamNode& paramNode, entity_id_t ent); void Deinit(); void HandleMessage(const CMessage& msg, bool global); void Serialize(ISerializer& serialize); void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent); template R Call(const char* funcname, const Ts&... params) const { R ret; if (m_ScriptInterface.CallFunction(m_Instance, funcname, ret, params...)) return ret; LOGERROR("Error calling component script function %s", funcname); return R(); } // CallRef is mainly used for returning script values with correct stack rooting. template void CallRef(const char* funcname, R ret, const Ts&... params) const { if (!m_ScriptInterface.CallFunction(m_Instance, funcname, ret, params...)) LOGERROR("Error calling component script function %s", funcname); } template void CallVoid(const char* funcname, const Ts&... params) const { if (!m_ScriptInterface.CallFunctionVoid(m_Instance, funcname, params...)) LOGERROR("Error calling component script function %s", funcname); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRootedValue m_Instance; - bool m_HasCustomSerialize; - bool m_HasCustomDeserialize; - bool m_HasNullSerialize; }; #endif // INCLUDED_SCRIPTCOMPONENT Index: ps/trunk/source/simulation2/serialization/BinarySerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/BinarySerializer.cpp (revision 24461) +++ ps/trunk/source/simulation2/serialization/BinarySerializer.cpp (revision 24462) @@ -1,446 +1,481 @@ /* Copyright (C) 2020 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 "BinarySerializer.h" #include "lib/alignment.h" #include "ps/CLogger.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "SerializedScriptTypes.h" static u8 GetArrayType(js::Scalar::Type arrayType) { switch(arrayType) { case js::Scalar::Int8: return SCRIPT_TYPED_ARRAY_INT8; case js::Scalar::Uint8: return SCRIPT_TYPED_ARRAY_UINT8; case js::Scalar::Int16: return SCRIPT_TYPED_ARRAY_INT16; case js::Scalar::Uint16: return SCRIPT_TYPED_ARRAY_UINT16; case js::Scalar::Int32: return SCRIPT_TYPED_ARRAY_INT32; case js::Scalar::Uint32: return SCRIPT_TYPED_ARRAY_UINT32; case js::Scalar::Float32: return SCRIPT_TYPED_ARRAY_FLOAT32; case js::Scalar::Float64: return SCRIPT_TYPED_ARRAY_FLOAT64; case js::Scalar::Uint8Clamped: return SCRIPT_TYPED_ARRAY_UINT8_CLAMPED; default: LOGERROR("Cannot serialize unrecognized typed array view: %d", arrayType); throw PSERROR_Serialize_InvalidScriptValue(); } } CBinarySerializerScriptImpl::CBinarySerializerScriptImpl(const ScriptInterface& scriptInterface, ISerializer& serializer) : m_ScriptInterface(scriptInterface), m_Serializer(serializer), m_ScriptBackrefsNext(0) { ScriptRequest rq(m_ScriptInterface); m_ScriptBackrefSymbol.init(rq.cx, JS::NewSymbol(rq.cx, nullptr)); } void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val) { ScriptRequest rq(m_ScriptInterface); switch (JS_TypeOfValue(rq.cx, val)) { case JSTYPE_UNDEFINED: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_VOID); break; } case JSTYPE_NULL: // This type is never actually returned (it's a JS2 feature) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_NULL); break; } case JSTYPE_OBJECT: { if (val.isNull()) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_NULL); break; } JS::RootedObject obj(rq.cx, &val.toObject()); // If we've already serialized this object, just output a reference to it i32 tag = GetScriptBackrefTag(obj); if (tag != -1) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_BACKREF); m_Serializer.NumberI32("tag", tag, 0, JSVAL_INT_MAX); break; } // Arrays, Maps and Sets are special cases of Objects bool isArray; bool isMap; bool isSet; if (JS::IsArrayObject(rq.cx, obj, &isArray) && isArray) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_ARRAY); // TODO: probably should have a more efficient storage format // Arrays like [1, 2, ] have an 'undefined' at the end which is part of the // length but seemingly isn't enumerated, so store the length explicitly uint length = 0; if (!JS::GetArrayLength(rq.cx, obj, &length)) throw PSERROR_Serialize_ScriptError("JS::GetArrayLength failed"); m_Serializer.NumberU32_Unbounded("array length", length); } else if (JS_IsTypedArrayObject(obj)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_TYPED_ARRAY); m_Serializer.NumberU8_Unbounded("array type", GetArrayType(JS_GetArrayBufferViewType(obj))); m_Serializer.NumberU32_Unbounded("byte offset", JS_GetTypedArrayByteOffset(obj)); m_Serializer.NumberU32_Unbounded("length", JS_GetTypedArrayLength(obj)); bool sharedMemory; // Now handle its array buffer // this may be a backref, since ArrayBuffers can be shared by multiple views JS::RootedValue bufferVal(rq.cx, JS::ObjectValue(*JS_GetArrayBufferViewBuffer(rq.cx, obj, &sharedMemory))); HandleScriptVal(bufferVal); break; } else if (JS::IsArrayBufferObject(obj)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_ARRAY_BUFFER); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: need to convert JS ArrayBuffer data to little-endian #endif u32 length = JS::GetArrayBufferByteLength(obj); m_Serializer.NumberU32_Unbounded("buffer length", length); JS::AutoCheckCannotGC nogc; bool sharedMemory; m_Serializer.RawBytes("buffer data", (const u8*)JS::GetArrayBufferData(obj, &sharedMemory, nogc), length); break; } else if (JS::IsMapObject(rq.cx, obj, &isMap) && isMap) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_MAP); m_Serializer.NumberU32_Unbounded("map size", JS::MapSize(rq.cx, obj)); JS::RootedValue keyValueIterator(rq.cx); if (!JS::MapEntries(rq.cx, obj, &keyValueIterator)) throw PSERROR_Serialize_ScriptError("JS::MapEntries failed"); JS::ForOfIterator it(rq.cx); if (!it.init(keyValueIterator)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::init failed"); JS::RootedValue keyValuePair(rq.cx); bool done; while (true) { if (!it.next(&keyValuePair, &done)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::next failed"); if (done) break; JS::RootedObject keyValuePairObj(rq.cx, &keyValuePair.toObject()); JS::RootedValue key(rq.cx); JS::RootedValue value(rq.cx); ENSURE(JS_GetElement(rq.cx, keyValuePairObj, 0, &key)); ENSURE(JS_GetElement(rq.cx, keyValuePairObj, 1, &value)); HandleScriptVal(key); HandleScriptVal(value); } break; } else if (JS::IsSetObject(rq.cx, obj, &isSet) && isSet) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_SET); m_Serializer.NumberU32_Unbounded("set size", JS::SetSize(rq.cx, obj)); JS::RootedValue valueIterator(rq.cx); if (!JS::SetValues(rq.cx, obj, &valueIterator)) throw PSERROR_Serialize_ScriptError("JS::SetValues failed"); JS::ForOfIterator it(rq.cx); if (!it.init(valueIterator)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::init failed"); JS::RootedValue value(rq.cx); bool done; while (true) { if (!it.next(&value, &done)) throw PSERROR_Serialize_ScriptError("JS::ForOfIterator::next failed"); if (done) break; HandleScriptVal(value); } break; } else { // Find type of object const JSClass* jsclass = JS_GetClass(obj); if (!jsclass) throw PSERROR_Serialize_ScriptError("JS_GetClass failed"); JSProtoKey protokey = JSCLASS_CACHED_PROTO_KEY(jsclass); if (protokey == JSProto_Object) { - // Standard Object prototype - m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); + // Object class - check for user-defined prototype + JS::RootedObject proto(rq.cx); + if (!JS_GetPrototype(rq.cx, obj, &proto)) + throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed"); + + SPrototypeSerialization protoInfo = GetPrototypeInfo(rq, proto); + + if (protoInfo.name == "Object") + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT); + else + { + m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE); + m_Serializer.String("proto", wstring_from_utf8(protoInfo.name), 0, 256); + + // Does it have custom Serialize function? + // if so, we serialize the data it returns, rather than the object's properties directly + if (protoInfo.hasCustomSerialize) + { + // If serialize is null, don't serialize anything more + if (!protoInfo.hasNullSerialize) + { + JS::RootedValue data(rq.cx); + if (!m_ScriptInterface.CallFunction(val, "Serialize", &data)) + throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed"); + m_Serializer.ScriptVal("data", &data); + } + // Break here to skip the custom object property serialization logic below. + break; + } + } } else if (protokey == JSProto_Number) { // Standard Number object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_NUMBER); // Get primitive value double d; if (!JS::ToNumber(rq.cx, val, &d)) throw PSERROR_Serialize_ScriptError("JS::ToNumber failed"); m_Serializer.NumberDouble_Unbounded("value", d); break; } else if (protokey == JSProto_String) { // Standard String object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_STRING); // Get primitive value JS::RootedString str(rq.cx, JS::ToString(rq.cx, val)); if (!str) throw PSERROR_Serialize_ScriptError("JS_ValueToString failed"); ScriptString("value", str); break; } else if (protokey == JSProto_Boolean) { // Standard Boolean object m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_BOOLEAN); // Get primitive value bool b = JS::ToBoolean(val); m_Serializer.Bool("value", b); break; } else { // Unrecognized class LOGERROR("Cannot serialise JS objects with unrecognized class '%s'", jsclass->name); throw PSERROR_Serialize_InvalidScriptValue(); } } // Find all properties (ordered by insertion time) JS::Rooted ida(rq.cx, JS::IdVector(rq.cx)); if (!JS_Enumerate(rq.cx, obj, &ida)) throw PSERROR_Serialize_ScriptError("JS_Enumerate failed"); m_Serializer.NumberU32_Unbounded("num props", (u32)ida.length()); for (size_t i = 0; i < ida.length(); ++i) { JS::RootedId id(rq.cx, ida[i]); JS::RootedValue idval(rq.cx); JS::RootedValue propval(rq.cx); // Forbid getters, which might delete values and mess things up. JS::Rooted desc(rq.cx); if (!JS_GetPropertyDescriptorById(rq.cx, obj, id, &desc)) throw PSERROR_Serialize_ScriptError("JS_GetPropertyDescriptorById failed"); if (desc.hasGetterObject()) throw PSERROR_Serialize_ScriptError("Cannot serialize property getters"); // Get the property name as a string if (!JS_IdToValue(rq.cx, id, &idval)) throw PSERROR_Serialize_ScriptError("JS_IdToValue failed"); JS::RootedString idstr(rq.cx, JS::ToString(rq.cx, idval)); if (!idstr) throw PSERROR_Serialize_ScriptError("JS_ValueToString failed"); ScriptString("prop name", idstr); if (!JS_GetPropertyById(rq.cx, obj, id, &propval)) throw PSERROR_Serialize_ScriptError("JS_GetPropertyById failed"); HandleScriptVal(propval); } break; } case JSTYPE_FUNCTION: { // We can't serialise functions, but we can at least name the offender (hopefully) std::wstring funcname(L"(unnamed)"); JS::RootedFunction func(rq.cx, JS_ValueToFunction(rq.cx, val)); if (func) { JS::RootedString string(rq.cx, JS_GetFunctionId(func)); if (string) { if (JS_StringHasLatin1Chars(string)) { size_t length; JS::AutoCheckCannotGC nogc; const JS::Latin1Char* ch = JS_GetLatin1StringCharsAndLength(rq.cx, nogc, string, &length); if (ch && length > 0) funcname.assign(ch, ch + length); } else { size_t length; JS::AutoCheckCannotGC nogc; const char16_t* ch = JS_GetTwoByteStringCharsAndLength(rq.cx, nogc, string, &length); if (ch && length > 0) funcname.assign(ch, ch + length); } } } LOGERROR("Cannot serialise JS objects of type 'function': %s", utf8_from_wstring(funcname)); throw PSERROR_Serialize_InvalidScriptValue(); } case JSTYPE_STRING: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_STRING); JS::RootedString stringVal(rq.cx, val.toString()); ScriptString("string", stringVal); break; } case JSTYPE_NUMBER: { // To reduce the size of the serialized data, we handle integers and doubles separately. // We can't check for val.isInt32 and val.isDouble directly, because integer numbers are not guaranteed // to be represented as integers. A number like 33 could be stored as integer on the computer of one player // and as double on the other player's computer. That would cause out of sync errors in multiplayer games because // their binary representation and thus the hash would be different. double d; d = val.toNumber(); i32 integer; if (JS_DoubleIsInt32(d, &integer)) { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_INT); m_Serializer.NumberI32_Unbounded("value", integer); } else { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_DOUBLE); m_Serializer.NumberDouble_Unbounded("value", d); } break; } case JSTYPE_BOOLEAN: { m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_BOOLEAN); bool b = val.toBoolean(); m_Serializer.NumberU8_Unbounded("value", b ? 1 : 0); break; } default: { debug_warn(L"Invalid TypeOfValue"); throw PSERROR_Serialize_InvalidScriptValue(); } } } void CBinarySerializerScriptImpl::ScriptString(const char* name, JS::HandleString string) { ScriptRequest rq(m_ScriptInterface); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: probably need to convert JS strings to little-endian #endif size_t length; JS::AutoCheckCannotGC nogc; // Serialize strings directly as UTF-16 or Latin1, to avoid expensive encoding conversions bool isLatin1 = JS_StringHasLatin1Chars(string); m_Serializer.Bool("isLatin1", isLatin1); if (isLatin1) { const JS::Latin1Char* chars = JS_GetLatin1StringCharsAndLength(rq.cx, nogc, string, &length); if (!chars) throw PSERROR_Serialize_ScriptError("JS_GetLatin1StringCharsAndLength failed"); m_Serializer.NumberU32_Unbounded("string length", (u32)length); m_Serializer.RawBytes(name, (const u8*)chars, length); } else { const char16_t* chars = JS_GetTwoByteStringCharsAndLength(rq.cx, nogc, string, &length); if (!chars) throw PSERROR_Serialize_ScriptError("JS_GetTwoByteStringCharsAndLength failed"); m_Serializer.NumberU32_Unbounded("string length", (u32)length); m_Serializer.RawBytes(name, (const u8*)chars, length*2); } } i32 CBinarySerializerScriptImpl::GetScriptBackrefTag(JS::HandleObject obj) { // To support non-tree structures (e.g. "var x = []; var y = [x, x];"), we need a way // to indicate multiple references to one object(/array). So every time we serialize a // new object, we give it a new tag; when we serialize it a second time we just refer // to that tag. // // Tags are stored on the object. To avoid overwriting any existing property, // they are saved as a uniquely-named, non-enumerable property (the serializer's unique symbol). ScriptRequest rq(m_ScriptInterface); JS::RootedValue symbolValue(rq.cx, JS::SymbolValue(m_ScriptBackrefSymbol)); JS::RootedId symbolId(rq.cx); ENSURE(JS_ValueToId(rq.cx, symbolValue, &symbolId)); JS::RootedValue tagValue(rq.cx); // If it was already there, return the tag bool tagFound; ENSURE(JS_HasPropertyById(rq.cx, obj, symbolId, &tagFound)); if (tagFound) { ENSURE(JS_GetPropertyById(rq.cx, obj, symbolId, &tagValue)); ENSURE(tagValue.isInt32()); return tagValue.toInt32(); } tagValue = JS::Int32Value(m_ScriptBackrefsNext); - JS_SetPropertyById(rq.cx, obj, symbolId, tagValue); + // TODO: this fails if the object cannot be written to. + // This means we could end up in an infinite loop... + if (!JS_DefinePropertyById(rq.cx, obj, symbolId, tagValue, JSPROP_READONLY)) + { + // For now just warn, this should be user-fixable and may not actually error out. + JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj.get())); + LOGWARNING("Serialization symbol cannot be written on object %s", m_ScriptInterface.ToString(&objVal)); + } ++m_ScriptBackrefsNext; // Return a non-tag number so callers know they need to serialize the object return -1; } Index: ps/trunk/source/simulation2/serialization/DebugSerializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/DebugSerializer.cpp (revision 24461) +++ ps/trunk/source/simulation2/serialization/DebugSerializer.cpp (revision 24462) @@ -1,179 +1,193 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 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 "DebugSerializer.h" #include "scriptinterface/ScriptInterface.h" #include "lib/secure_crt.h" #include "lib/utf8.h" #include "ps/CStr.h" #include #include /* * The output format here is intended to be compatible with YAML, * so it is human readable and usable in diff and can also be parsed with * external tools. */ // MSVC and GCC give slightly different serializations of floats // (e.g. "1e+010" vs "1e+10"). To make the debug serialization easily comparable // across platforms, we want to convert to a canonical form. // TODO: we just do e+0xx now; ought to handle varying precisions and inf and nan etc too template std::string canonfloat(T value, int prec) { std::stringstream str; str << std::setprecision(prec) << value; std::string r = str.str(); size_t e = r.find('e'); if (e == r.npos) // no "e" return r; if (e == r.length() - 5 && r[e + 2] == '0') // e.g. "1e+010" r.erase(e + 2, 1); return r; } CDebugSerializer::CDebugSerializer(const ScriptInterface& scriptInterface, std::ostream& stream, bool includeDebugInfo) : m_ScriptInterface(scriptInterface), m_Stream(stream), m_IsDebug(includeDebugInfo), m_Indent(0) { } void CDebugSerializer::Indent(int spaces) { m_Indent += spaces; } void CDebugSerializer::Dedent(int spaces) { ENSURE(spaces <= m_Indent); m_Indent -= spaces; } #define INDENT std::string(m_Indent, ' ') void CDebugSerializer::Comment(const std::string& comment) { m_Stream << INDENT << "# " << comment << "\n"; } void CDebugSerializer::TextLine(const std::string& text) { m_Stream << INDENT << text << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint8_t value) { m_Stream << INDENT << name << ": " << (int)value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int8_t value) { m_Stream << INDENT << name << ": " << (int)value << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint16_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int16_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, uint32_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, int32_t value) { m_Stream << INDENT << name << ": " << value << "\n"; } void CDebugSerializer::PutNumber(const char* name, float value) { m_Stream << INDENT << name << ": " << canonfloat(value, 8) << "\n"; } void CDebugSerializer::PutNumber(const char* name, double value) { m_Stream << INDENT << name << ": " << canonfloat(value, 17) << "\n"; } void CDebugSerializer::PutNumber(const char* name, fixed value) { m_Stream << INDENT << name << ": " << value.ToString() << "\n"; } void CDebugSerializer::PutBool(const char* name, bool value) { m_Stream << INDENT << name << ": " << (value ? "true" : "false") << "\n"; } void CDebugSerializer::PutString(const char* name, const std::string& value) { std::string escaped; escaped.reserve(value.size()); for (size_t i = 0; i < value.size(); ++i) if (value[i] == '"') escaped += "\\\""; else if (value[i] == '\\') escaped += "\\\\"; else if (value[i] == '\n') escaped += "\\n"; else escaped += value[i]; m_Stream << INDENT << name << ": " << "\"" << escaped << "\"\n"; } void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value) { - std::string source = m_ScriptInterface.ToString(value, true); + ScriptRequest rq(m_ScriptInterface); - m_Stream << INDENT << name << ": " << source << "\n"; + JS::RootedValue serialize(rq.cx); + if (m_ScriptInterface.GetProperty(value, "Serialize", &serialize) && !serialize.isNullOrUndefined()) + { + // If the value has a Serialize property, pretty-parse that and return the value as a raw string. + // This gives more debug data for components in case of OOS. + m_ScriptInterface.CallFunction(value, "Serialize", &serialize); + std::string serialized_source = m_ScriptInterface.ToString(&serialize, true); + std::string source = m_ScriptInterface.ToString(value, false); + m_Stream << INDENT << name << ": " << serialized_source << " (raw: " << source << ")\n"; + } + else + { + std::string source = m_ScriptInterface.ToString(value, true); + m_Stream << INDENT << name << ": " << source << "\n"; + } } void CDebugSerializer::PutRaw(const char* name, const u8* data, size_t len) { m_Stream << INDENT << name << ": (" << len << " bytes)"; char buf[4]; for (size_t i = 0; i < len; ++i) { sprintf_s(buf, ARRAY_SIZE(buf), " %02x", (unsigned int)data[i]); m_Stream << buf; } m_Stream << "\n"; } bool CDebugSerializer::IsDebug() const { return m_IsDebug; } std::ostream& CDebugSerializer::GetStream() { return m_Stream; } Index: ps/trunk/source/simulation2/serialization/IDeserializer.h =================================================================== --- ps/trunk/source/simulation2/serialization/IDeserializer.h (revision 24461) +++ ps/trunk/source/simulation2/serialization/IDeserializer.h (revision 24462) @@ -1,94 +1,97 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2020 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_IDESERIALIZER #define INCLUDED_IDESERIALIZER #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptTypes.h" ERROR_GROUP(Deserialize); ERROR_TYPE(Deserialize, OutOfBounds); ERROR_TYPE(Deserialize, InvalidCharInString); ERROR_TYPE(Deserialize, ReadFailed); ERROR_TYPE(Deserialize, ScriptError); /** * Deserialization interface; see \ref serialization "serialization overview". */ class IDeserializer { public: virtual ~IDeserializer(); virtual void NumberU8(const char* name, uint8_t& out, uint8_t lower, uint8_t upper); virtual void NumberI8(const char* name, int8_t& out, int8_t lower, int8_t upper); virtual void NumberU16(const char* name, uint16_t& out, uint16_t lower, uint16_t upper); virtual void NumberI16(const char* name, int16_t& out, int16_t lower, int16_t upper); virtual void NumberU32(const char* name, uint32_t& out, uint32_t lower, uint32_t upper); virtual void NumberI32(const char* name, int32_t& out, int32_t lower, int32_t upper); virtual void NumberU8_Unbounded(const char* name, uint8_t& out); virtual void NumberI8_Unbounded(const char* name, int8_t& out); virtual void NumberU16_Unbounded(const char* name, uint16_t& out); virtual void NumberI16_Unbounded(const char* name, int16_t& out); virtual void NumberU32_Unbounded(const char* name, uint32_t& out); virtual void NumberI32_Unbounded(const char* name, int32_t& out); virtual void NumberFloat_Unbounded(const char* name, float& out); virtual void NumberDouble_Unbounded(const char* name, double& out); virtual void NumberFixed_Unbounded(const char* name, fixed& out); virtual void Bool(const char* name, bool& out); virtual void StringASCII(const char* name, std::string& out, uint32_t minlength, uint32_t maxlength); virtual void String(const char* name, std::wstring& out, uint32_t minlength, uint32_t maxlength); /// Deserialize a JS::Value, replacing 'out' virtual void ScriptVal(const char* name, JS::MutableHandleValue out) = 0; - /// Deserialize an object value, appending properties to object 'objVal' - virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal) = 0; + /** + * Deserialize an object and assign its properties to objVal + * (Essentially equivalent to Object.assign(objVal, serialized)) + */ + virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal) = 0; /// Deserialize a JSString virtual void ScriptString(const char* name, JS::MutableHandleString out) = 0; virtual void RawBytes(const char* name, u8* data, size_t len); // Features for simulation-state serialisation: virtual int GetVersion() const; /** * Returns a stream which can be used to deserialize data directly. * (This is particularly useful for chaining multiple deserializers * together.) */ virtual std::istream& GetStream() = 0; /** * Throws an exception if the stream definitely cannot provide the required * number of bytes. * (It might be conservative and *not* throw an exception in some cases where * the stream actually can't provide the required bytes.) * (This should be used when allocating memory based on data in the * stream, e.g. reading strings, to avoid dangerously large allocations * when the data is invalid.) */ virtual void RequireBytesInStream(size_t numBytes) = 0; protected: virtual void Get(const char* name, u8* data, size_t len) = 0; }; #endif // INCLUDED_IDESERIALIZER Index: ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h =================================================================== --- ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h (revision 24461) +++ ps/trunk/source/simulation2/serialization/SerializedScriptTypes.h (revision 24462) @@ -1,56 +1,94 @@ /* Copyright (C) 2020 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_SERIALIZEDSCRIPTTYPES #define INCLUDED_SERIALIZEDSCRIPTTYPES enum { SCRIPT_TYPE_VOID = 0, SCRIPT_TYPE_NULL = 1, SCRIPT_TYPE_ARRAY = 2, SCRIPT_TYPE_OBJECT = 3, // standard Object prototype SCRIPT_TYPE_STRING = 4, SCRIPT_TYPE_INT = 5, SCRIPT_TYPE_DOUBLE = 6, SCRIPT_TYPE_BOOLEAN = 7, SCRIPT_TYPE_BACKREF = 8, SCRIPT_TYPE_TYPED_ARRAY = 9, // ArrayBufferView subclasses - see below SCRIPT_TYPE_ARRAY_BUFFER = 10, // ArrayBuffer containing actual typed array data (may be shared by multiple views) - SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // user-defined prototype - currently unused + SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // User-defined prototype - see GetPrototypeInfo SCRIPT_TYPE_OBJECT_NUMBER = 12, // standard Number class SCRIPT_TYPE_OBJECT_STRING = 13, // standard String class SCRIPT_TYPE_OBJECT_BOOLEAN = 14, // standard Boolean class SCRIPT_TYPE_OBJECT_MAP = 15, // Map class SCRIPT_TYPE_OBJECT_SET = 16 // Set class }; // ArrayBufferView subclasses (to avoid relying directly on the JSAPI enums) enum { SCRIPT_TYPED_ARRAY_INT8 = 0, SCRIPT_TYPED_ARRAY_UINT8 = 1, SCRIPT_TYPED_ARRAY_INT16 = 2, SCRIPT_TYPED_ARRAY_UINT16 = 3, SCRIPT_TYPED_ARRAY_INT32 = 4, SCRIPT_TYPED_ARRAY_UINT32 = 5, SCRIPT_TYPED_ARRAY_FLOAT32 = 6, SCRIPT_TYPED_ARRAY_FLOAT64 = 7, SCRIPT_TYPED_ARRAY_UINT8_CLAMPED = 8 }; +struct SPrototypeSerialization +{ + std::string name = ""; + bool hasCustomSerialize = false; + bool hasCustomDeserialize = false; + bool hasNullSerialize = false; +}; + +inline SPrototypeSerialization GetPrototypeInfo(const ScriptRequest& rq, JS::HandleObject prototype) +{ + SPrototypeSerialization ret; + + JS::RootedValue constructor(rq.cx, JS::ObjectOrNullValue(JS_GetConstructor(rq.cx, prototype))); + if (!ScriptInterface::GetProperty(rq, constructor, "name", ret.name)) + throw PSERROR_Serialize_ScriptError("Could not get constructor name."); + + // Nothing to do for basic Object objects. + if (ret.name == "Object") + return ret; + + if (!JS_HasProperty(rq.cx, prototype, "Serialize", &ret.hasCustomSerialize) || + !JS_HasProperty(rq.cx, prototype, "Deserialize", &ret.hasCustomDeserialize)) + throw PSERROR_Serialize_ScriptError("JS_HasProperty failed"); + + if (ret.hasCustomSerialize) + { + JS::RootedValue serialize(rq.cx); + if (!JS_GetProperty(rq.cx, prototype, "Serialize", &serialize)) + throw PSERROR_Serialize_ScriptError("JS_GetProperty failed"); + + if (serialize.isNull()) + ret.hasNullSerialize = true; + else if (!ret.hasCustomDeserialize) + throw PSERROR_Serialize_ScriptError("Cannot serialize script with non-null Serialize but no Deserialize."); + } + return ret; +} + #endif // INCLUDED_SERIALIZEDSCRIPTTYPES Index: ps/trunk/source/simulation2/serialization/StdDeserializer.cpp =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 24461) +++ ps/trunk/source/simulation2/serialization/StdDeserializer.cpp (revision 24462) @@ -1,443 +1,490 @@ /* Copyright (C) 2020 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 "StdDeserializer.h" -#include "SerializedScriptTypes.h" -#include "StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE - +#include "lib/byte_order.h" +#include "ps/CStr.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" // For typed arrays and ArrayBuffer - -#include "lib/byte_order.h" +#include "simulation2/serialization/ISerializer.h" +#include "simulation2/serialization/SerializedScriptTypes.h" +#include "simulation2/serialization/StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) : m_ScriptInterface(scriptInterface), m_Stream(stream) { JS_AddExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); } CStdDeserializer::~CStdDeserializer() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface.GetGeneralJSContext(), CStdDeserializer::Trace, this); } void CStdDeserializer::Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void CStdDeserializer::TraceMember(JSTracer *trc) { for (JS::Heap& backref : m_ScriptBackrefs) JS::TraceEdge(trc, &backref, "StdDeserializer::m_ScriptBackrefs"); } void CStdDeserializer::Get(const char* name, u8* data, size_t len) { #if DEBUG_SERIALIZER_ANNOTATE std::string strName; char c = m_Stream.get(); ENSURE(c == '<'); while (1) { c = m_Stream.get(); if (c == '>') break; else strName += c; } ENSURE(strName == name); #else UNUSED2(name); #endif m_Stream.read((char*)data, (std::streamsize)len); if (!m_Stream.good()) { // hit eof before len, or other errors // NOTE: older libc++ versions incorrectly set eofbit on the last char; test gcount as a workaround // see https://llvm.org/bugs/show_bug.cgi?id=9335 if (m_Stream.bad() || m_Stream.fail() || (m_Stream.eof() && m_Stream.gcount() != (std::streamsize)len)) throw PSERROR_Deserialize_ReadFailed(); } } std::istream& CStdDeserializer::GetStream() { return m_Stream; } void CStdDeserializer::RequireBytesInStream(size_t numBytes) { // It would be nice to do: // if (numBytes > (size_t)m_Stream.rdbuf()->in_avail()) // throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); // but that doesn't work (at least on MSVC) since in_avail isn't // guaranteed to return the actual number of bytes available; see e.g. // http://social.msdn.microsoft.com/Forums/en/vclanguage/thread/13009a88-933f-4be7-bf3d-150e425e66a6#70ea562d-8605-4742-8851-1bae431ce6ce // Instead we'll just verify that it's not an extremely large number: if (numBytes > 64*MiB) throw PSERROR_Deserialize_OutOfBounds("RequireBytesInStream"); } void CStdDeserializer::AddScriptBackref(JS::HandleObject obj) { m_ScriptBackrefs.push_back(JS::Heap(obj)); } void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret) { ENSURE(m_ScriptBackrefs.size() > tag); ret.set(m_ScriptBackrefs[tag]); } //////////////////////////////////////////////////////////////// -JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject appendParent) +JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject preexistingObject) { ScriptRequest rq(m_ScriptInterface); uint8_t type; NumberU8_Unbounded("type", type); switch (type) { case SCRIPT_TYPE_VOID: return JS::UndefinedValue(); case SCRIPT_TYPE_NULL: return JS::NullValue(); case SCRIPT_TYPE_ARRAY: case SCRIPT_TYPE_OBJECT: + case SCRIPT_TYPE_OBJECT_PROTOTYPE: { JS::RootedObject obj(rq.cx); - if (appendParent) - { - obj.set(appendParent); - } - else if (type == SCRIPT_TYPE_ARRAY) + if (type == SCRIPT_TYPE_ARRAY) { u32 length; NumberU32_Unbounded("array length", length); obj.set(JS::NewArrayObject(rq.cx, length)); } - else // SCRIPT_TYPE_OBJECT + else if (type == SCRIPT_TYPE_OBJECT) { obj.set(JS_NewPlainObject(rq.cx)); } + else // SCRIPT_TYPE_OBJECT_PROTOTYPE + { + CStrW prototypeName; + String("proto", prototypeName, 0, 256); + + // If an object was passed, no need to construct a new one. + if (preexistingObject != nullptr) + obj.set(preexistingObject); + else + { + JS::RootedValue constructor(rq.cx); + if (!ScriptInterface::GetGlobalProperty(rq, prototypeName.ToUTF8(), &constructor)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object"); + + JS::RootedObject newObj(rq.cx); + if (!JS::Construct(rq.cx, constructor, JS::HandleValueArray::empty(), &newObj)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object"); + obj.set(newObj); + } + + JS::RootedObject prototype(rq.cx); + JS_GetPrototype(rq.cx, obj, &prototype); + SPrototypeSerialization info = GetPrototypeInfo(rq, prototype); + + if (preexistingObject != nullptr && prototypeName != wstring_from_utf8(info.name)) + throw PSERROR_Deserialize_ScriptError("Deserializer failed: incorrect pre-existing object"); + + + if (info.hasCustomDeserialize) + { + AddScriptBackref(obj); + + // If Serialize is null, we'll still call Deserialize but with undefined argument + JS::RootedValue data(rq.cx); + if (!info.hasNullSerialize) + ScriptVal("data", &data); + + JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj)); + m_ScriptInterface.CallFunctionVoid(objVal, "Deserialize", data); + + return JS::ObjectValue(*obj); + } + else if (info.hasNullSerialize) + { + // If we serialized null, this means we're pretty much a default-constructed object. + // Nothing to do. + AddScriptBackref(obj); + return JS::ObjectValue(*obj); + } + } if (!obj) throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object"); AddScriptBackref(obj); uint32_t numProps; NumberU32_Unbounded("num props", numProps); bool isLatin1; for (uint32_t i = 0; i < numProps; ++i) { Bool("isLatin1", isLatin1); if (isLatin1) { std::vector propname; ReadStringLatin1("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); utf16string prp(propname.begin(), propname.end());; // TODO: Should ask upstream about getting a variant of JS_SetProperty with a length param. if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)prp.data(), prp.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } else { utf16string propname; ReadStringUTF16("prop name", propname); JS::RootedValue propval(rq.cx, ReadScriptVal("prop value", nullptr)); if (!JS_SetUCProperty(rq.cx, obj, (const char16_t*)propname.data(), propname.length(), propval)) throw PSERROR_Deserialize_ScriptError(); } } return JS::ObjectValue(*obj); } case SCRIPT_TYPE_STRING: { JS::RootedString str(rq.cx); ScriptString("string", &str); return JS::StringValue(str); } case SCRIPT_TYPE_INT: { int32_t value; NumberI32("value", value, JSVAL_INT_MIN, JSVAL_INT_MAX); return JS::NumberValue(value); } case SCRIPT_TYPE_DOUBLE: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue rval(rq.cx, JS::NumberValue(value)); if (rval.isNull()) throw PSERROR_Deserialize_ScriptError("JS_NewNumberValue failed"); return rval; } case SCRIPT_TYPE_BOOLEAN: { uint8_t value; NumberU8("value", value, 0, 1); return JS::BooleanValue(value ? true : false); } case SCRIPT_TYPE_BACKREF: { i32 tag; NumberI32("tag", tag, 0, JSVAL_INT_MAX); JS::RootedObject obj(rq.cx); GetScriptBackref(tag, &obj); if (!obj) throw PSERROR_Deserialize_ScriptError("Invalid backref tag"); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_NUMBER: { double value; NumberDouble_Unbounded("value", value); JS::RootedValue val(rq.cx, JS::NumberValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Number, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_STRING: { JS::RootedString str(rq.cx); ScriptString("value", &str); if (!str) throw PSERROR_Deserialize_ScriptError(); JS::RootedValue val(rq.cx, JS::StringValue(str)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_String, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_OBJECT_BOOLEAN: { bool value; Bool("value", value); JS::RootedValue val(rq.cx, JS::BooleanValue(value)); JS::RootedObject ctorobj(rq.cx); if (!JS_GetClassObject(rq.cx, JSProto_Boolean, &ctorobj)) throw PSERROR_Deserialize_ScriptError("JS_GetClassObject failed"); JS::RootedObject obj(rq.cx, JS_New(rq.cx, ctorobj, JS::HandleValueArray(val))); if (!obj) throw PSERROR_Deserialize_ScriptError("JS_New failed"); AddScriptBackref(obj); return JS::ObjectValue(*obj); } case SCRIPT_TYPE_TYPED_ARRAY: { u8 arrayType; u32 byteOffset, length; NumberU8_Unbounded("array type", arrayType); NumberU32_Unbounded("byte offset", byteOffset); NumberU32_Unbounded("length", length); // To match the serializer order, we reserve the typed array's backref tag here JS::RootedObject arrayObj(rq.cx); AddScriptBackref(arrayObj); // Get buffer object JS::RootedValue bufferVal(rq.cx, ReadScriptVal("buffer", nullptr)); if (!bufferVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject bufferObj(rq.cx, &bufferVal.toObject()); if (!JS::IsArrayBufferObject(bufferObj)) throw PSERROR_Deserialize_ScriptError("js_IsArrayBuffer failed"); switch(arrayType) { case SCRIPT_TYPED_ARRAY_INT8: arrayObj = JS_NewInt8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8: arrayObj = JS_NewUint8ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT16: arrayObj = JS_NewInt16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT16: arrayObj = JS_NewUint16ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_INT32: arrayObj = JS_NewInt32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT32: arrayObj = JS_NewUint32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT32: arrayObj = JS_NewFloat32ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_FLOAT64: arrayObj = JS_NewFloat64ArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; case SCRIPT_TYPED_ARRAY_UINT8_CLAMPED: arrayObj = JS_NewUint8ClampedArrayWithBuffer(rq.cx, bufferObj, byteOffset, length); break; default: throw PSERROR_Deserialize_ScriptError("Failed to deserialize unrecognized typed array view"); } if (!arrayObj) throw PSERROR_Deserialize_ScriptError("js_CreateTypedArrayWithBuffer failed"); return JS::ObjectValue(*arrayObj); } case SCRIPT_TYPE_ARRAY_BUFFER: { u32 length; NumberU32_Unbounded("buffer length", length); #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: need to convert JS ArrayBuffer data from little-endian #endif void* contents = malloc(length); ENSURE(contents); RawBytes("buffer data", (u8*)contents, length); JS::RootedObject bufferObj(rq.cx, JS::NewArrayBufferWithContents(rq.cx, length, contents)); AddScriptBackref(bufferObj); return JS::ObjectValue(*bufferObj); } case SCRIPT_TYPE_OBJECT_MAP: { JS::RootedObject obj(rq.cx, JS::NewMapObject(rq.cx)); AddScriptBackref(obj); u32 mapSize; NumberU32_Unbounded("map size", mapSize); for (u32 i=0; i& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len); str.resize(len); Get(name, (u8*)str.data(), len); } void CStdDeserializer::ReadStringUTF16(const char* name, utf16string& str) { uint32_t len; NumberU32_Unbounded("string length", len); RequireBytesInStream(len*2); str.resize(len); Get(name, (u8*)str.data(), len*2); } void CStdDeserializer::ScriptString(const char* name, JS::MutableHandleString out) { #if BYTE_ORDER != LITTLE_ENDIAN #error TODO: probably need to convert JS strings from little-endian #endif ScriptRequest rq(m_ScriptInterface); bool isLatin1; Bool("isLatin1", isLatin1); if (isLatin1) { std::vector str; ReadStringLatin1(name, str); out.set(JS_NewStringCopyN(rq.cx, (const char*)str.data(), str.size())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewStringCopyN failed"); } else { utf16string str; ReadStringUTF16(name, str); out.set(JS_NewUCStringCopyN(rq.cx, (const char16_t*)str.data(), str.length())); if (!out) throw PSERROR_Deserialize_ScriptError("JS_NewUCStringCopyN failed"); } } void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out) { out.set(ReadScriptVal(name, nullptr)); } -void CStdDeserializer::ScriptObjectAppend(const char* name, JS::HandleValue objVal) +void CStdDeserializer::ScriptObjectAssign(const char* name, JS::HandleValue objVal) { ScriptRequest rq(m_ScriptInterface); if (!objVal.isObject()) throw PSERROR_Deserialize_ScriptError(); JS::RootedObject obj(rq.cx, &objVal.toObject()); ReadScriptVal(name, obj); } Index: ps/trunk/source/simulation2/serialization/StdDeserializer.h =================================================================== --- ps/trunk/source/simulation2/serialization/StdDeserializer.h (revision 24461) +++ ps/trunk/source/simulation2/serialization/StdDeserializer.h (revision 24462) @@ -1,63 +1,63 @@ /* Copyright (C) 2020 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_STDDESERIALIZER #define INCLUDED_STDDESERIALIZER #include "IDeserializer.h" #include "ps/utf16string.h" #include #include class CStdDeserializer : public IDeserializer { NONCOPYABLE(CStdDeserializer); public: CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream); virtual ~CStdDeserializer(); virtual void ScriptVal(const char* name, JS::MutableHandleValue out); - virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal); + virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal); virtual void ScriptString(const char* name, JS::MutableHandleString out); virtual std::istream& GetStream(); virtual void RequireBytesInStream(size_t numBytes); static void Trace(JSTracer *trc, void *data); void TraceMember(JSTracer *trc); protected: virtual void Get(const char* name, u8* data, size_t len); private: - JS::Value ReadScriptVal(const char* name, JS::HandleObject appendParent); + JS::Value ReadScriptVal(const char* name, JS::HandleObject preexistingObject); void ReadStringLatin1(const char* name, std::vector& str); void ReadStringUTF16(const char* name, utf16string& str); virtual void AddScriptBackref(JS::HandleObject obj); virtual void GetScriptBackref(size_t tag, JS::MutableHandleObject ret); std::vector > m_ScriptBackrefs; const ScriptInterface& m_ScriptInterface; std::istream& m_Stream; }; #endif // INCLUDED_STDDESERIALIZER Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 24461) +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 24462) @@ -1,902 +1,902 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "lib/self_test.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/components/ICmpTemplateManager.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TS_ASSERT_THROWS_PSERROR(e, t, s) \ TS_ASSERT_THROWS_EQUALS(e, const t& ex, std::string(ex.what()), s) class TestComponentManager : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_Load() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); } void test_LookupCID() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT_EQUALS(man.LookupCID("Test1A"), (int)CID_Test1A); TS_ASSERT_EQUALS(man.LookupCID("Test1B"), (int)CID_Test1B); } void test_AllocateNewEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)4); TS_ASSERT_EQUALS(man.AllocateNewEntity(100), (u32)100); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)101); // TODO: // TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)102); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY+1); man.ResetState(); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); } void test_rng() { // Ensure we get the same random number with the same seed double first; { CSimContext context; CComponentManager man(context, g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(first)) TS_FAIL("Couldn't get random number!"); } double second; { CSimContext context; CComponentManager man(context, g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(second)) TS_FAIL("Couldn't get random number!"); } TS_ASSERT_EQUALS(first, second); } void test_AddComponent_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); CEntityHandle hnd1 = man.AllocateEntityHandle(1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_Test1A, noParam)); { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, 12345, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Invalid component id 12345"); } { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, CID_Test1B, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Multiple components for interface "); } } void test_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test1B, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); } void test_SendMessage() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1B, noParam); man.AddComponent(hnd3, CID_Test2A, noParam); man.AddComponent(hnd4, CID_Test1A, noParam); man.AddComponent(hnd4, CID_Test2A, noParam); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(100)); CMessageInterpolate msg3(0, 0, 0); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); // Test_1A subscribed locally to msg1, nothing subscribed globally man.PostMessage(ent1, msg1); man.PostMessage(ent1, msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21050); // Test_1B, Test_2A subscribed locally to msg2, nothing subscribed globally man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12010); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); // Test_1A subscribed locally to msg3, Test_1B subscribed globally man.BroadcastMessage(msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11004); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12030); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); man.PostMessage(ent1, msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11006); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12050); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local - skipped TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); } void test_ParamNode() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); } void test_script_basic() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test.js")); TS_ASSERT_EQUALS(man.LookupCID("TestScript1A"), (int)CID__LastNative); TS_ASSERT_EQUALS(man.LookupCID("TestScript1B"), (int)CID__LastNative+1); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript2A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(25)); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201025); } void test_script_helper_basic() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-helper.js")); TS_ASSERT(man.LoadScript(L"simulation/helpers/test-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Helper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 3); } void test_script_global_helper() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-global-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_GlobalHelper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 2); } void test_script_interface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/interfaces/test-interface.js")); TS_ASSERT(man.LoadScript(L"simulation/components/test-interface.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Interface"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2_Interface"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000 + IID__LastNative); } void test_script_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TestLogger log; TS_ASSERT(man.LoadScript(L"simulation/components/error.js")); // The following exception is caught and dropped by the JS script, and should not appear in the logs. TS_ASSERT_STR_NOT_CONTAINS(log.GetOutput(), "ERROR: JavaScript error: simulation/components/error.js line 4\nInvalid interface id"); // The following exception is not caught by the JS script. TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: No script wrapper found for interface id 12345"); } void test_script_entityID() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-entityid.js")); entity_id_t ent1 = 1, ent2 = 234; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } void test_script_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-query.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 400); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 21000); } void test_script_AddEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = ent1 + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_AddLocalEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = man.AllocateNewLocalEntity() + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddLocalEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_DestroyEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-destroyentity.js")); entity_id_t ent1 = 10; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_DestroyEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); man.FlushDestroyedComponents(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) == NULL); } void test_script_messages() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-msg.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 100); // This GetX broadcasts messages TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 200); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 650); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 5150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 26050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 5650); } void test_script_template() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1110000"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_Init"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_Init"), testParam.GetChild("node")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1+10+100+1000); } void test_script_template_readonly() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_readonly"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_readonly"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 102); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 102); } void test_script_hotload() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload1.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadB"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadC"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload2.js", true)); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); man.AddComponent(hnd3, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd4, man.LookupCID("HotloadB"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 200); } void test_script_modding() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding1.js")); man.AddComponent(hnd1, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding2.js")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); man.AddComponent(hnd2, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1000); } void test_serialization() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 10, ent2 = 20, ent3 = FIRST_LOCAL_ENTITY; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); man.AddComponent(hnd3, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" "- id: 10\n" " Test1A:\n" " x: 11000\n" " Test2A:\n" " x: 21000\n" "\n" "- id: 20\n" " Test1A:\n" " x: 1234\n" "\n" "- id: 536870912\n" " type: local\n" " Test2A:\n" " x: 21000\n" "\n" ); std::string hash; TS_ASSERT(man.ComputeStateHash(hash, false)); TS_ASSERT_EQUALS(hash.length(), (size_t)16); TS_ASSERT_SAME_DATA(hash.data(), "\x3c\x25\x6e\x22\x58\x23\x09\x58\x38\xca\xb2\x1e\x0b\x8c\xac\xcf", 16); // echo -en "\x05\x00\x00\x0078606\x02\0\0\0\x01\0\0\0\x0a\0\0\0\xf8\x2a\0\0\x14\0\0\0\xd2\x04\0\0\x04\0\0\0\x0a\0\0\0\x08\x52\0\0" | md5sum | perl -pe 's/([0-9a-f]{2})/\\x$1/g' // ^^^^^^^^ rng ^^^^^^^^ ^^next^^ ^^Test1A^^ ^^^ent1^^ ^^^11000^^^ ^^^ent2^^ ^^^1234^^^ ^^Test2A^^ ^^ent1^^ ^^^21000^^^ std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); TS_ASSERT_STREAM(stateStream, 73, "\x05\x00\x00\x00\x37\x38\x36\x30\x36" // RNG "\x02\x00\x00\x00" // next entity ID "\x00\x00\x00\x00" // num system component types "\x02\x00\x00\x00" // num component types "\x06\x00\x00\x00Test1A" "\x02\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\xf8\x2a\x00\x00" // 11000 "\x14\x00\x00\x00" // ent2 "\xd2\x04\x00\x00" // 1234 "\x06\x00\x00\x00Test2A" "\x01\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\x08\x52\x00\x00" // 21000 ); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man2.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); } void test_script_serialization() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_values"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_entity"), testParam); // TODO: Since the upgrade to SpiderMonkey v24 this test won't be able to correctly represent // non-tree structures because sharp variables were removed (bug 566700). // This also affects the debug serializer and it could make sense to implement correct serialization again. man.AddComponent(hnd3, man.LookupCID("TestScript1_nontree"), testParam); man.AddComponent(hnd4, man.LookupCID("TestScript1_custom"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 8); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n\ entities:\n\ - id: 1\n\ TestScript1_values:\n\ - object: {\n\ + comp: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ \"a\": 1,\n\ \"b\": \"2\",\n\ \"c\": [\n\ 3,\n\ \"4\",\n\ [\n\ 5,\n\ []\n\ ]\n\ ]\n\ }\n\ }\n\ \n\ - id: 2\n\ TestScript1_entity:\n\ - object: {}\n\ + comp: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ - object: ({x:[[2], [2], [], {y:[2]}]})\n\ + comp: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ - object: {\n\ + comp: {\n\ \"c\": 1\n\ -}\n\ +} (raw: ({y:2}))\n\ \n" ); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent3, IID_Test1))->GetX(), 12); } void test_script_serialization_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_getter"), noParam); std::stringstream stateStream; TS_ASSERT_THROWS_PSERROR(man.SerializeState(stateStream), PSERROR_Serialize_ScriptError, "Cannot serialize property getters"); // (The script will die if the getter is executed) } void test_script_serialization_template() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); man.InitSystemEntity(); entity_id_t ent2 = 2; CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; // The template manager takes care of reloading templates on deserialization, // so we need to use it here TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); const CParamNode* testParam = tempMan->LoadTemplate(ent2, "template-serialize"); man.AddComponent(hnd2, man.LookupCID("TestScript1_consts"), testParam->GetChild("TestScript1_consts")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12347); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 12347); } void test_dynamic_subscription() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test1), true); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test2), true); man.DestroyComponentsSoon(ent1); man.FlushDestroyedComponents(); } }; Index: ps/trunk/source/simulation2/tests/test_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 24461) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 24462) @@ -1,885 +1,931 @@ /* Copyright (C) 2020 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 "lib/self_test.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "callgrind.h" #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n"); } void test_Debug_floats() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); serialize.NumberFloat_Unbounded("x", 1e-4f); serialize.NumberFloat_Unbounded("x", 1e5f); serialize.NumberFloat_Unbounded("x", 1e-5f); serialize.NumberFloat_Unbounded("x", 1e6f); serialize.NumberFloat_Unbounded("x", 1e-6f); serialize.NumberFloat_Unbounded("x", 1e10f); serialize.NumberFloat_Unbounded("x", 1e-10f); serialize.NumberDouble_Unbounded("x", 1e4); serialize.NumberDouble_Unbounded("x", 1e-4); serialize.NumberDouble_Unbounded("x", 1e5); serialize.NumberDouble_Unbounded("x", 1e-5); serialize.NumberDouble_Unbounded("x", 1e6); serialize.NumberDouble_Unbounded("x", 1e-6); serialize.NumberDouble_Unbounded("x", 1e10); serialize.NumberDouble_Unbounded("x", 1e-10); serialize.NumberDouble_Unbounded("x", 1e100); serialize.NumberDouble_Unbounded("x", 1e-100); serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4)); TS_ASSERT_STR_EQUALS(stream.str(), "x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n" "x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n" "x: 10000\n" ); } void test_Debug_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.Comment("comment"); serialize_types(serialize); TS_ASSERT_STR_EQUALS(stream.str(), "# comment\n" "i8: -123\n" "u8: 255\n" "i16: -12345\n" "u16: 56789\n" "i32: -123\n" "u32: 4294967173\n" "float: 1e+30\n" "double: 1.0000000000000001e+300\n" "fixed: 1234.5\n" "t: true\n" "f: false\n" "string: \"example\"\n" "string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\"" "string 3: \"example\\n\\ntest\"\n" "string 4: \"t\xC3\xAAst\"\n" "raw bytes: (6 bytes) 00 01 02 03 0f 10\n" ); } void test_Std_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00"); CStdDeserializer deserialize(script, stream); int32_t n; deserialize.NumberI32_Unbounded("x", n); TS_ASSERT_EQUALS(n, -123); deserialize.NumberI32_Unbounded("y", n); TS_ASSERT_EQUALS(n, 1234); deserialize.NumberI32("z", n, 0, 65535); TS_ASSERT_EQUALS(n, 12345); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Std_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize_types(serialize); CStdDeserializer deserialize(script, stream); int8_t i8v; uint8_t u8v; int16_t i16v; uint16_t u16v; int32_t i32v; uint32_t u32v; float flt; double dbl; fixed fxd; bool bl; std::string str; std::wstring wstr; u8 cbuf[256]; deserialize.NumberI8_Unbounded("i8", i8v); TS_ASSERT_EQUALS(i8v, -123); deserialize.NumberU8_Unbounded("u8", u8v); TS_ASSERT_EQUALS(u8v, 255); deserialize.NumberI16_Unbounded("i16", i16v); TS_ASSERT_EQUALS(i16v, -12345); deserialize.NumberU16_Unbounded("u16", u16v); TS_ASSERT_EQUALS(u16v, 56789); deserialize.NumberI32_Unbounded("i32", i32v); TS_ASSERT_EQUALS(i32v, -123); deserialize.NumberU32_Unbounded("u32", u32v); TS_ASSERT_EQUALS(u32v, 4294967173u); deserialize.NumberFloat_Unbounded("float", flt); TS_ASSERT_EQUALS(flt, 1e+30f); deserialize.NumberDouble_Unbounded("double", dbl); TS_ASSERT_EQUALS(dbl, 1e+300); deserialize.NumberFixed_Unbounded("fixed", fxd); TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5); deserialize.Bool("t", bl); TS_ASSERT_EQUALS(bl, true); deserialize.Bool("f", bl); TS_ASSERT_EQUALS(bl, false); deserialize.StringASCII("string", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example"); deserialize.StringASCII("string 2", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\"\\\""); deserialize.StringASCII("string 3", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\n\ntest"); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; deserialize.String("string 4", wstr, 0, 255); TS_ASSERT_WSTR_EQUALS(wstr, testw); cbuf[6] = 0x42; // sentinel deserialize.RawBytes("raw bytes", cbuf, 6); TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Hash_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_Hash_stream() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer hashSerialize(script); hashSerialize.NumberI32_Unbounded("x", -123); hashSerialize.NumberU32_Unbounded("y", 1234); hashSerialize.NumberI32("z", 12345, 0, 65535); ISerializer& serialize = hashSerialize; { CStdSerializer streamSerialize(script, serialize.GetStream()); streamSerialize.NumberI32_Unbounded("x2", -456); streamSerialize.NumberU32_Unbounded("y2", 5678); streamSerialize.NumberI32("z2", 45678, 0, 65535); } TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_bounds() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); serialize.NumberI32("x", -16, -16, 16); TS_ASSERT_THROWS(serialize.NumberI32("x", 99, -16, 16), const PSERROR_Serialize_OutOfBounds&); TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), const PSERROR_Serialize_OutOfBounds&); } // TODO: test exceptions more thoroughly void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL) { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); JS::RootedValue obj(rq.cx); TSM_ASSERT(msg, script.Eval(input, &obj)); if (debug) { std::stringstream dbgstream; CDebugSerializer serialize(script, dbgstream); serialize.ScriptVal("script", &obj); TS_ASSERT_STR_EQUALS(dbgstream.str(), debug); } std::stringstream stream; CStdSerializer serialize(script, stream); serialize.ScriptVal("script", &obj); if (expstream) { TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream); } CStdDeserializer deserialize(script, stream); JS::RootedValue newobj(rq.cx); deserialize.ScriptVal("script", &newobj); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); + std::stringstream stream2; + CStdSerializer serialize2(script, stream2); + CStdDeserializer deserialize2(script, stream2); + + // Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized. + serialize2.ScriptVal("script2", &newobj); + deserialize2.ScriptVal("script2", &newobj); + std::string source; TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } + void test_script_fancy_objects() + { + // This asserts that objects are deserialized with their correct prototypes. + helper_script_roundtrip("Custom Object", "" + "function customObj() { this.a = this.customFunc.name; };" + "customObj.prototype.customFunc = function customFunc(){};" + "new customObj();", "({a:\"customFunc\"})"); + + helper_script_roundtrip("Custom Class", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " customFunc(){};" + "}; new customObj();", "({a:\"customFunc\"})"); + + helper_script_roundtrip("Custom Class with Serialize/Deserialize()", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " Serialize() { return { 'foo': 'bar' }; }" + " Deserialize(data) { this.foo = data.foo; }" + " customFunc(){};" + "}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})"); + + helper_script_roundtrip("Custom Class with null serialize & deserialize()", "" + "class customObj {" + " constructor() { this.a = this.customFunc.name; }" + " Deserialize(data) { this.test = 'test'; };" + " customFunc(){};" + "}; customObj.prototype.Serialize=null;" + "new customObj();", "({a:\"customFunc\", test:\"test\"})"); + + helper_script_roundtrip("Custom Class with arguments but still works", "" + "class customObj {" + " constructor(test) { this.a = test; }" + " Serialize() { return { 'data': this.a }; };" + " Deserialize(data) { this.a = data.data; };" + "}; new customObj(4);", "({a:4})"); + } + void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); CALLGRIND_START_INSTRUMENTATION; size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } CALLGRIND_STOP_INSTRUMENTATION; t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world delete &g_TexMan; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } };