Index: ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js +++ ps/trunk/binaries/data/mods/public/simulation/components/ProductionQueue.js @@ -63,7 +63,6 @@ // "timeRemaining": 10000, // msecs // } - this.timer = undefined; // this.ProgressInterval msec timer, active while the queue is non-empty this.paused = false; this.entityCache = []; @@ -345,7 +344,6 @@ if (this.queue.length < this.MaxQueueSize) { - if (type == "unit") { if (!Number.isInteger(count) || count <= 0) @@ -462,15 +460,10 @@ if (!cmpPlayer.TrySubtractResources(cost)) return; - // Tell the technology manager that we have started researching this so that people can't research the same - // thing twice. + // Tell the technology manager that we have started researching this + // such that people can't research the same thing twice. let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); cmpTechnologyManager.QueuedResearch(templateName, this.entity); - if (!this.queue.length) - { - cmpTechnologyManager.StartedResearch(templateName, false); - this.SetAnimation("researching"); - } let time = techCostMultiplier.time * (template.researchTime || 0) * 1000; this.queue.push({ @@ -498,14 +491,10 @@ return; } - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); - // If this is the first item in the queue, start the timer. if (!this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, {}); - } + this.StartTimer(); } else { @@ -582,10 +571,8 @@ this.SetAnimation("idle"); } - // Remove from the queue. - // (We don't need to remove the timer - it'll expire if it discovers the queue is empty.) this.queue.splice(i, 1); - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); return; } @@ -669,12 +656,7 @@ { // Reset the queue to refund any resources. this.ResetQueue(); - - if (this.timer) - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - cmpTimer.CancelTimer(this.timer); - } + this.StopTimer(); }; /* @@ -788,12 +770,13 @@ }; /* - * Increments progress on the first batch in the production queue, and blocks the + * Increments progress on the first item in the production queue and blocks the * queue if population limit is reached or some units failed to spawn. + * @param {Object} data - Unused in this case. + * @param {number} lateness - The time passed since the expected time to fire the function. */ -ProductionQueue.prototype.ProgressTimeout = function(data) +ProductionQueue.prototype.ProgressTimeout = function(data, lateness) { - // Check if the production is paused (eg the entity is garrisoned) if (this.paused) return; @@ -804,10 +787,10 @@ // Allocate available time to as many queue items as it takes // until we've used up all the time (so that we work accurately // with items that take fractions of a second). - let time = this.ProgressInterval; + let time = this.ProgressInterval + lateness; let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - while (time > 0 && this.queue.length) + while (this.queue.length) { let item = this.queue[0]; if (!item.productionStarted) @@ -832,15 +815,14 @@ // (we'll try again on the next timeout). cmpPlayer.BlockTraining(); - break; + return; } cmpPlayer.UnBlockTraining(); + Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity }); } - - if (item.technologyTemplate) + else if (item.technologyTemplate) { - // Mark the research as started. let cmpTechnologyManager = QueryOwnerInterface(this.entity, IID_TechnologyManager); if (cmpTechnologyManager) cmpTechnologyManager.StartedResearch(item.technologyTemplate, true); @@ -851,45 +833,35 @@ } item.productionStarted = true; - if (item.unitTemplate) - Engine.PostMessage(this.entity, MT_TrainingStarted, { "entity": this.entity }); } // If we won't finish the batch now, just update its timer. if (item.timeRemaining > time) { item.timeRemaining -= time; - // send a message for the AIs. - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); - break; + // Send a message for the AIs. + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); + return; } if (item.unitTemplate) { let numSpawned = this.SpawnUnits(item.unitTemplate, item.count, item.metadata); + if (numSpawned) + cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); if (numSpawned == item.count) { - // All entities spawned, this batch finished. - cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); - time -= item.timeRemaining; - this.queue.shift(); - // Unset flag that training is blocked. cmpPlayer.UnBlockTraining(); this.spawnNotified = false; - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); } else { if (numSpawned > 0) { - // Training is only partially finished. - cmpPlayer.UnReservePopulationSlots(item.population * numSpawned); item.count -= numSpawned; - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, null); } - // Some entities failed to spawn. - // Set flag that training is blocked. cmpPlayer.BlockTraining(); if (!this.spawnNotified) @@ -902,7 +874,7 @@ }); this.spawnNotified = true; } - break; + return; } } else if (item.technologyTemplate) @@ -921,41 +893,57 @@ if (cmpSoundManager) cmpSoundManager.PlaySoundGroup(template.soundComplete, this.entity); } - - time -= item.timeRemaining; - - this.queue.shift(); - Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); } + + time -= item.timeRemaining; + this.queue.shift(); + Engine.PostMessage(this.entity, MT_ProductionQueueChanged, {}); } - // If the queue's empty, delete the timer, else repeat it. if (!this.queue.length) { - this.timer = undefined; + this.StopTimer(); // Unset flag that training is blocked. // (This might happen when the player unqueues all batches.) cmpPlayer.UnBlockTraining(); } - else - { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, data); - } }; ProductionQueue.prototype.PauseProduction = function() { - this.timer = undefined; + this.StopTimer(); this.paused = true; }; ProductionQueue.prototype.UnpauseProduction = function() { this.paused = false; - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - this.timer = cmpTimer.SetTimeout(this.entity, IID_ProductionQueue, "ProgressTimeout", this.ProgressInterval, {}); + this.StartTimer(); +}; + +ProductionQueue.prototype.StartTimer = function() +{ + if (this.timer) + return; + + this.timer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).SetInterval( + this.entity, + IID_ProductionQueue, + "ProgressTimeout", + this.ProgressInterval, + this.ProgressInterval, + null + ); +}; + +ProductionQueue.prototype.StopTimer = function() +{ + if (!this.timer) + return; + + Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).CancelTimer(this.timer); + delete this.timer; }; ProductionQueue.prototype.OnValueModification = function(msg) Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_ProductionQueue.js @@ -11,6 +11,7 @@ Engine.LoadComponentScript("interfaces/Trigger.js"); Engine.LoadComponentScript("interfaces/Upgrade.js"); Engine.LoadComponentScript("EntityLimits.js"); +Engine.LoadComponentScript("Timer.js"); Engine.RegisterGlobal("Resources", { "BuildSchema": (a, b) => {} @@ -215,7 +216,8 @@ }); AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetTimeout": (ent, iid, func) => {} + "SetInterval": (ent, iid, func) => 1, + "CancelTimer": (id) => {} }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { @@ -274,7 +276,7 @@ TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); - cmpProdQueue.ProgressTimeout(); + cmpProdQueue.ProgressTimeout(null, 0); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 3); TS_ASSERT_EQUALS(cmpEntLimits.GetMatchCounts().some_template, 3); @@ -289,7 +291,7 @@ }); cmpProdQueue.AddBatch("some_template", "unit", 3); - cmpProdQueue.ProgressTimeout(); + cmpProdQueue.ProgressTimeout(null, 0); TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); TS_ASSERT_EQUALS(cmpEntLimits.GetCounts().some_limit, 6); @@ -324,7 +326,7 @@ }); AddMock(SYSTEM_ENTITY, IID_Timer, { - "SetTimeout": (ent, iid, func) => {} + "SetInterval": (ent, iid, func) => 1 }); AddMock(SYSTEM_ENTITY, IID_TemplateManager, { @@ -382,6 +384,97 @@ TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); } +function test_batch_removal() +{ + let playerEnt = 2; + let playerID = 1; + let testEntity = 3; + + ConstructComponent(playerEnt, "EntityLimits", { + "Limits": { + "some_limit": 8 + }, + "LimitChangers": {}, + "LimitRemovers": {} + }); + + AddMock(SYSTEM_ENTITY, IID_GuiInterface, { + "PushNotification": () => {} + }); + + AddMock(SYSTEM_ENTITY, IID_Trigger, { + "CallEvent": () => {} + }); + + ConstructComponent(SYSTEM_ENTITY, "Timer", null); + + AddMock(SYSTEM_ENTITY, IID_TemplateManager, { + "TemplateExists": () => true, + "GetTemplate": name => ({ + "Cost": { + "BuildTime": 0, + "Population": 1, + "Resources": {} + }, + "TrainingRestrictions": { + "Category": "some_limit" + } + }) + }); + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": id => playerEnt + }); + + let cmpPlayer = AddMock(playerEnt, IID_Player, { + "GetCiv": () => "iber", + "GetPlayerID": () => playerID, + "GetTimeMultiplier": () => 0, + "BlockTraining": () => {}, + "UnBlockTraining": () => {}, + "UnReservePopulationSlots": () => {}, + "TrySubtractResources": () => true, + "AddResources": () => {}, + "TryReservePopulationSlots": () => 1 + }); + let cmpPlayerBlockSpy = new Spy(cmpPlayer, "BlockTraining"); + let cmpPlayerUnblockSpy = new Spy(cmpPlayer, "UnBlockTraining"); + + AddMock(testEntity, IID_Ownership, { + "GetOwner": () => playerID + }); + + let cmpProdQueue = ConstructComponent(testEntity, "ProductionQueue", { + "Entities": { "_string": "some_template" }, + "BatchTimeModifier": 1 + }); + + cmpProdQueue.AddBatch("some_template", "unit", 3); + TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); + cmpProdQueue.ProgressTimeout(null, 0); + TS_ASSERT_EQUALS(cmpPlayerBlockSpy._called, 1); + + cmpProdQueue.AddBatch("some_template", "unit", 2); + TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 2); + + cmpProdQueue.RemoveBatch(1); + TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 1); + TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 0); + + cmpProdQueue.RemoveBatch(2); + TS_ASSERT_EQUALS(cmpProdQueue.GetQueue().length, 0); + cmpProdQueue.ProgressTimeout(null, 0); + TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 1); + + cmpProdQueue.AddBatch("some_template", "unit", 3); + cmpProdQueue.AddBatch("some_template", "unit", 3); + cmpPlayer.TryReservePopulationSlots = () => false; + cmpProdQueue.RemoveBatch(3); + cmpProdQueue.ProgressTimeout(null, 0); + TS_ASSERT_EQUALS(cmpPlayerUnblockSpy._called, 2); + +} + function test_token_changes() { const ent = 10; @@ -401,7 +494,9 @@ "GetCiv": () => "test", "GetDisabledTemplates": () => [], "GetDisabledTechnologies": () => [], + "TryReservePopulationSlots": () => false, // Always have pop space. "TrySubtractResources": () => true, + "UnBlockTraining": () => {}, "AddResources": () => {}, "GetPlayerID": () => 1, // entitylimits @@ -453,4 +548,5 @@ testEntitiesList(); regression_test_d1879(); test_batch_adding(); +test_batch_removal(); test_token_changes();