1 #include "ProductionQueue.h"
2
3 #include "Empire.h"
4 #include "../universe/BuildingType.h"
5 #include "../universe/Condition.h"
6 #include "../universe/ShipHull.h"
7 #include "../universe/ShipPart.h"
8 #include "../universe/ShipDesign.h"
9 #include "../universe/ValueRef.h"
10 #include "../util/AppInterface.h"
11 #include "../util/GameRules.h"
12 #include "../util/ScopedTimer.h"
13 #include "../util/i18n.h"
14
15 #include <boost/range/numeric.hpp>
16 #include <boost/range/adaptor/map.hpp>
17 #include <boost/uuid/uuid_io.hpp>
18
19
20 namespace {
21 const float EPSILON = 0.001f;
22
AddRules(GameRules & rules)23 void AddRules(GameRules& rules) {
24 // limits amount of PP per turn that can be imported into the stockpile
25 rules.Add<bool>("RULE_STOCKPILE_IMPORT_LIMITED",
26 "RULE_STOCKPILE_IMPORT_LIMITED_DESC",
27 "", false, true);
28
29 rules.Add<double>("RULE_PRODUCTION_QUEUE_FRONTLOAD_FACTOR",
30 "RULE_PRODUCTION_QUEUE_FRONTLOAD_FACTOR_DESC",
31 "", 0.0, true, RangedValidator<double>(0.0, 30.0));
32 rules.Add<double>("RULE_PRODUCTION_QUEUE_TOPPING_UP_FACTOR",
33 "RULE_PRODUCTION_QUEUE_TOPPING_UP_FACTOR_DESC",
34 "", 0.0, true, RangedValidator<double>(0.0, 30.0));
35 }
36 bool temp_bool = RegisterGameRules(&AddRules);
37
38 // Calculates per-turn limit on PP contribution, taking into account unit
39 // item cost, min build turns, blocksize, remaining repeat count, current
40 // progress, and other potential factors discussed below.
41
42 // RULE_PRODUCTION_QUEUE_FRONTLOAD_FACTOR and
43 // RULE_PRODUCTION_QUEUE_TOPPING_UP_FACTOR specify how the ProductionQueue
44 // will limit allocation towards building a given item on a given turn.
45 // The base amount of maximum allocation per turn (if the player has enough
46 // PP available) is the item's total cost, divided over its minimum build
47 // time. Sometimes complications arise, though, which unexpectedly delay
48 // the completion even if the item had been fully-funded every turn,
49 // because costs have risen partway through (such as due to increasing ship
50 // costs resulting from recent ship constructoin completion and ensuing
51 // increase of Fleet Maintenance costs.
52 // These two settings provide a mechanism for some allocation leeway to deal
53 // with mid-build cost increases without causing the project completion to
54 // take an extra turn because of the small bit of increased cost. The
55 // settings differ in the timing of the extra allocation allowed.
56 // Both factors have a minimum value of 0.0 and a maximum value of 0.3.
57
58 // Making the frontloaded factor greater than zero increases the per-turn
59 // allocation cap by the specified percentage (so it always spreads the
60 // extra allocation across all turns). Making the topping-up option nonzero
61 // allows the final turn allocation cap to be increased by the specified
62 // percentage of the total cost, if needed (and then subject toavailability
63 // of course). They can both be nonzero, although to avoid that introducing
64 // too much interaction complexity into the minimum build time safeguard for
65 // topping-up, the topping-up percentage will be reduced by the
66 // frontloading setting.
67
68 // Note that for very small values of the options (less than 5%), when
69 // dealing with very low cost items the effect/protection may be noticeably
70 // less than expected because of interactions with the ProductionQueue
71 // Epsilon value (0.01)
72
CalculateProductionPerTurnLimit(const ProductionQueue::Element & queue_element,float item_cost,int build_turns)73 float CalculateProductionPerTurnLimit(const ProductionQueue::Element& queue_element,
74 float item_cost, int build_turns)
75 {
76 const float frontload_limit_factor = GetGameRules().Get<double>("RULE_PRODUCTION_QUEUE_FRONTLOAD_FACTOR") * 0.01;
77 // any allowed topping up is limited by how much frontloading was allowed
78 const float topping_up_limit_factor =
79 std::max(0.0, GetGameRules().Get<double>("RULE_PRODUCTION_QUEUE_TOPPING_UP_FACTOR") * 0.01 - frontload_limit_factor);
80
81 item_cost *= queue_element.blocksize;
82 build_turns = std::max(build_turns, 1);
83 float element_accumulated_PP = queue_element.progress*item_cost; // effective PP accumulated by this element towards producing next item. progress is a fraction between 0 and 1.
84 float element_total_cost = item_cost * queue_element.remaining; // total PP to produce all items in this element
85 float additional_pp_to_complete_element =
86 element_total_cost - element_accumulated_PP; // additional PP, beyond already-accumulated PP, to produce all items in this element
87 float additional_pp_to_complete_item = item_cost - element_accumulated_PP; // additional PP, beyond already-accumulated PP, to produce the current item of this element
88 float basic_element_per_turn_limit = item_cost / build_turns;
89 // the extra constraints on frontload and topping up amounts ensure that won't let complete in less than build_turns (so long as costs do not decrease)
90 float frontload = (1.0f + frontload_limit_factor/std::max(build_turns-1,1)) *
91 basic_element_per_turn_limit - 2.0f * EPSILON;
92 float topping_up_limit = basic_element_per_turn_limit +
93 std::min(topping_up_limit_factor * item_cost, basic_element_per_turn_limit - 2 * EPSILON);
94 float topping_up = (additional_pp_to_complete_item < topping_up_limit) ?
95 additional_pp_to_complete_item : basic_element_per_turn_limit;
96 float retval = std::min(additional_pp_to_complete_element,
97 std::max(basic_element_per_turn_limit,
98 std::max(frontload, topping_up)));
99 //DebugLogger() << "CalculateProductionPerTurnLimit for item " << queue_element.item.build_type << " " << queue_element.item.name
100 // << " " << queue_element.item.design_id << " : accumPP: " << element_accumulated_PP << " pp_to_complete_elem: "
101 // << additional_pp_to_complete_element << " pp_to_complete_item: " << additional_pp_to_complete_item
102 // << " basic_element_per_turn_limit: " << basic_element_per_turn_limit << " frontload: " << frontload
103 // << " topping_up_limit: " << topping_up_limit << " topping_up: " << topping_up << " retval: " << retval;
104
105 return retval;
106 }
107
CalculateNewStockpile(int empire_id,float starting_stockpile,float project_transfer_to_stockpile,const std::map<std::set<int>,float> & available_pp,const std::map<std::set<int>,float> & allocated_pp,const std::map<std::set<int>,float> & allocated_stockpile_pp)108 float CalculateNewStockpile(int empire_id, float starting_stockpile, float project_transfer_to_stockpile,
109 const std::map<std::set<int>, float>& available_pp,
110 const std::map<std::set<int>, float>& allocated_pp,
111 const std::map<std::set<int>, float>& allocated_stockpile_pp)
112 {
113 TraceLogger() << "CalculateNewStockpile for empire " << empire_id;
114 const Empire* empire = GetEmpire(empire_id);
115 if (!empire) {
116 ErrorLogger() << "CalculateStockpileContribution() passed null empire. doing nothing.";
117 return 0.0f;
118 }
119 float stockpile_limit = empire->GetProductionQueue().StockpileCapacity();
120 float stockpile_used = boost::accumulate(allocated_stockpile_pp | boost::adaptors::map_values, 0.0f);
121 TraceLogger() << " ... stockpile limit: " << stockpile_limit << " used: " << stockpile_used << " starting: " << starting_stockpile;
122 float new_contributions = 0.0f;
123 for (auto const& available_group : available_pp) {
124 auto alloc_it = allocated_pp.find(available_group.first);
125 float allocated_here = (alloc_it == allocated_pp.end())
126 ? 0.0f : alloc_it->second;
127 float excess_here = available_group.second - allocated_here;
128 if (excess_here < EPSILON)
129 continue;
130 // Transfer excess to stockpile
131 new_contributions += excess_here;
132 TraceLogger() << "...allocated in group: " << allocated_here
133 << " excess in group: " << excess_here
134 << " to stockpile: " << new_contributions;
135 }
136
137 if ((new_contributions + project_transfer_to_stockpile) > stockpile_limit &&
138 GetGameRules().Get<bool>("RULE_STOCKPILE_IMPORT_LIMITED"))
139 { new_contributions = stockpile_limit - project_transfer_to_stockpile; }
140
141 return starting_stockpile + new_contributions + project_transfer_to_stockpile - stockpile_used;
142 }
143
144 /** Sets the allocated_pp value for each Element in the passed
145 * ProductionQueue \a queue. Elements are allocated PP based on their need,
146 * the limits they can be given per turn, and the amount available at their
147 * production location (which is itself limited by the resource supply
148 * system groups that are able to exchange resources with the build
149 * location and the amount of minerals and industry produced in the group).
150 * Elements will not receive funding if they cannot be produced by the
151 * empire with the indicated \a empire_id this turn at their build location.
152 * Also checks if elements will be completed this turn.
153 * Returns the amount of PP which gets transferred to the stockpile using
154 * stockpile project build items. */
SetProdQueueElementSpending(std::map<std::set<int>,float> available_pp,float available_stockpile,float stockpile_limit,const std::vector<std::set<int>> & queue_element_resource_sharing_object_groups,const std::map<std::pair<ProductionQueue::ProductionItem,int>,std::pair<float,int>> & queue_item_costs_and_times,const std::vector<bool> & is_producible,ProductionQueue::QueueType & queue,std::map<std::set<int>,float> & allocated_pp,std::map<std::set<int>,float> & allocated_stockpile_pp,int & projects_in_progress,bool simulating)155 float SetProdQueueElementSpending(
156 std::map<std::set<int>, float> available_pp, float available_stockpile,
157 float stockpile_limit,
158 const std::vector<std::set<int>>& queue_element_resource_sharing_object_groups,
159 const std::map<std::pair<ProductionQueue::ProductionItem, int>,
160 std::pair<float, int>>& queue_item_costs_and_times,
161 const std::vector<bool>& is_producible,
162 ProductionQueue::QueueType& queue,
163 std::map<std::set<int>, float>& allocated_pp,
164 std::map<std::set<int>, float>& allocated_stockpile_pp,
165 int& projects_in_progress, bool simulating)
166 {
167 //DebugLogger() << "========SetProdQueueElementSpending========";
168 //DebugLogger() << "production status: ";
169 //DebugLogger() << "queue: ";
170 //for (const ProductionQueue::Element& elem : queue)
171 // DebugLogger() << " ... name: " << elem.item.name << "id: " << elem.item.design_id << " allocated: " << elem.allocated_pp << " locationid: " << elem.location << " ordered: " << elem.ordered;
172
173 if (queue.size() != queue_element_resource_sharing_object_groups.size()) {
174 ErrorLogger() << "SetProdQueueElementSpending queue size and sharing groups size inconsistent. aborting";
175 return 0.0f;
176 }
177
178 // See explanation at CalculateProductionPerTurnLimit() above regarding operation of these factors.
179 // any allowed topping up is limited by how much frontloading was allowed
180 //const float frontload_limit_factor = GetGameRules().Get<double>("RULE_PRODUCTION_QUEUE_FRONTLOAD_FACTOR") * 0.01;
181 //const float topping_up_limit_factor = std::max(0.0, GetGameRules().Get<double>("RULE_PRODUCTION_QUEUE_TOPPING_UP_FACTOR") * 0.01f - frontload_limit_factor);
182 // DebugLogger() << "SetProdQueueElementSpending frontload factor " << frontload_limit_factor;
183 // DebugLogger() << "SetProdQueueElementSpending topping up factor " << topping_up_limit_factor;
184
185 projects_in_progress = 0;
186 allocated_pp.clear();
187 allocated_stockpile_pp.clear();
188 float dummy_pp_source = 0.0f;
189 float stockpile_transfer = 0.0f;
190 //DebugLogger() << "queue size: " << queue.size();
191 int i = 0;
192 for (auto& queue_element : queue) {
193 queue_element.allocated_pp = 0.0f; // default, to be updated below...
194 if (queue_element.paused) {
195 TraceLogger() << "allocation: " << queue_element.allocated_pp
196 << " to: " << queue_element.item.name
197 << " due to it being paused";
198 ++i;
199 continue;
200 }
201
202 // get resource sharing group and amount of resource available to build this item
203 const auto& group = queue_element_resource_sharing_object_groups[i];
204 auto available_pp_it = available_pp.find(group);
205 float& group_pp_available = (available_pp_it != available_pp.end()) ?
206 available_pp_it->second : dummy_pp_source;
207
208 if ((group_pp_available <= 0) &&
209 (available_stockpile <= 0 || !queue_element.allowed_imperial_stockpile_use))
210 {
211 TraceLogger() << "allocation: " << queue_element.allocated_pp
212 << " to: " << queue_element.item.name
213 << " due to lack of available PP in group";
214 queue_element.allocated_pp = 0.0f;
215 ++i;
216 continue;
217 }
218
219 //DebugLogger() << "group has " << group_pp_available << " PP available";
220
221 // see if item is producible this turn...
222 if (!is_producible[i]) {
223 // can't be produced at this location this turn.
224 queue_element.allocated_pp = 0.0f;
225 TraceLogger() << "allocation: " << queue_element.allocated_pp
226 << " to unproducible item: " << queue_element.item.name;
227 ++i;
228 continue;
229 }
230
231 // get max contribution per turn and turns to build at max contribution rate
232 int location_id = (queue_element.item.CostIsProductionLocationInvariant() ?
233 INVALID_OBJECT_ID : queue_element.location);
234 std::pair<ProductionQueue::ProductionItem, int> key(queue_element.item, location_id);
235 float item_cost = 1e6; // dummy/default value, shouldn't ever really be needed
236 int build_turns = 1; // dummy/default value, shouldn't ever really be needed
237 auto time_cost_it = queue_item_costs_and_times.find(key);
238 if (time_cost_it != queue_item_costs_and_times.end()) {
239 item_cost = time_cost_it->second.first;
240 build_turns = time_cost_it->second.second;
241 } else {
242 ErrorLogger() << "item: " << queue_element.item.name
243 << " somehow failed time cost lookup for location " << location_id;
244 }
245 //DebugLogger() << "item " << queue_element.item.name << " costs " << item_cost << " for " << build_turns << " turns";
246
247 float element_this_turn_limit = CalculateProductionPerTurnLimit(queue_element, item_cost, build_turns);
248
249 // determine how many pp to allocate to this queue element block this turn. allocation is limited by the
250 // item cost, which is the max number of PP per turn that can be put towards this item, and by the
251 // total cost remaining to complete the last item in the queue element (eg. the element has all but
252 // the last item complete already) and by the total pp available in this element's production location's
253 // resource sharing group (including any stockpile availability)
254 float stockpile_available_for_this =
255 (queue_element.allowed_imperial_stockpile_use) ? available_stockpile : 0;
256
257 float allocation = std::max(0.0f,
258 std::min(element_this_turn_limit,
259 group_pp_available + stockpile_available_for_this));
260
261 if (queue_element.item.build_type == BT_STOCKPILE) {
262 if (GetGameRules().Get<bool>("RULE_STOCKPILE_IMPORT_LIMITED")) {
263 float unused_limit = std::max(0.0f, stockpile_limit - stockpile_transfer);
264 allocation = std::min(allocation, unused_limit);
265 }
266 }
267
268 //DebugLogger() << "element accumulated " << element_accumulated_PP << " of total cost "
269 // << element_total_cost << " and needs " << additional_pp_to_complete_element
270 // << " more to be completed";
271 //DebugLogger() << "... allocating " << allocation;
272
273 // allocate pp
274 queue_element.allocated_pp = std::max(allocation, EPSILON);
275
276 // record allocation from group
277 float group_drawdown = std::min(allocation, group_pp_available);
278
279 allocated_pp[group] += group_drawdown; // relies on default initial mapped value of 0.0f
280 if (queue_element.item.build_type == BT_STOCKPILE) {
281 stockpile_transfer += group_drawdown;
282 }
283 group_pp_available -= group_drawdown;
284
285 float stockpile_drawdown = allocation <= group_drawdown ? 0.0f : (allocation - group_drawdown);
286 TraceLogger() << "allocation: " << allocation
287 << " to: " << queue_element.item.name
288 << " from group: " << group_drawdown
289 << " from stockpile: " << stockpile_drawdown
290 << " to stockpile:" << stockpile_transfer
291 << " group remaining: " << group_pp_available;
292
293 // record allocation from stockpile
294 // protect against any slight mismatch that might possible happen from multiplying
295 // and dividing by a very very small stockpile_conversion_rate
296 stockpile_drawdown = std::min(stockpile_drawdown, available_stockpile);
297 if (stockpile_drawdown > 0) {
298 allocated_stockpile_pp[group] += stockpile_drawdown;
299 available_stockpile -= stockpile_drawdown;
300 }
301
302 // check for completion
303 float block_cost = item_cost * queue_element.blocksize;
304 if (block_cost*(1.0f - queue_element.progress) - queue_element.allocated_pp < EPSILON)
305 queue_element.turns_left_to_next_item = 1;
306
307 // if simulating, update progress
308 if (simulating)
309 queue_element.progress += allocation / std::max(EPSILON, block_cost); // add turn's progress due to allocation
310
311 if (allocation > 0.0f)
312 ++projects_in_progress;
313
314 ++i;
315 }
316 return stockpile_transfer;
317 }
318 }
319
320
ProductionItem()321 ProductionQueue::ProductionItem::ProductionItem()
322 {}
323
ProductionItem(BuildType build_type_)324 ProductionQueue::ProductionItem::ProductionItem(BuildType build_type_) :
325 build_type(build_type_)
326 {
327 if (build_type_ == BT_STOCKPILE)
328 name = UserStringNop("PROJECT_BT_STOCKPILE");
329 }
330
ProductionItem(BuildType build_type_,std::string name_)331 ProductionQueue::ProductionItem::ProductionItem(BuildType build_type_, std::string name_) :
332 build_type(build_type_),
333 name(name_)
334 {}
335
ProductionItem(BuildType build_type_,int design_id_)336 ProductionQueue::ProductionItem::ProductionItem(BuildType build_type_, int design_id_) :
337 build_type(build_type_),
338 design_id(design_id_)
339 {
340 if (build_type == BT_SHIP) {
341 if (const ShipDesign* ship_design = GetShipDesign(design_id))
342 name = ship_design->Name();
343 else
344 ErrorLogger() << "ProductionItem::ProductionItem couldn't get ship design with id: " << design_id;
345 }
346 }
347
CostIsProductionLocationInvariant() const348 bool ProductionQueue::ProductionItem::CostIsProductionLocationInvariant() const {
349 if (build_type == BT_BUILDING) {
350 const BuildingType* type = GetBuildingType(name);
351 if (!type)
352 return true;
353 return type->ProductionCostTimeLocationInvariant();
354
355 } else if (build_type == BT_SHIP) {
356 const ShipDesign* design = GetShipDesign(design_id);
357 if (!design)
358 return true;
359 return design->ProductionCostTimeLocationInvariant();
360
361 } else if (build_type == BT_STOCKPILE) {
362 return true;
363 }
364 return false;
365 }
366
EnqueueConditionPassedAt(int location_id) const367 bool ProductionQueue::ProductionItem::EnqueueConditionPassedAt(int location_id) const {
368 switch (build_type) {
369 case BT_BUILDING: {
370 if (const BuildingType* bt = GetBuildingType(name)) {
371 auto location_obj = Objects().get(location_id);
372 auto c = bt->EnqueueLocation();
373 if (!c)
374 return true;
375 return c->Eval(ScriptingContext(location_obj), location_obj);
376 }
377 return true;
378 break;
379 }
380 case BT_SHIP: // ships don't have enqueue location conditions
381 case BT_STOCKPILE: // stockpile can always be enqueued
382 default:
383 return true;
384 }
385 }
386
operator <(const ProductionItem & rhs) const387 bool ProductionQueue::ProductionItem::operator<(const ProductionItem& rhs) const {
388 if (build_type < rhs.build_type)
389 return true;
390 if (build_type > rhs.build_type)
391 return false;
392 if (build_type == BT_BUILDING)
393 return name < rhs.name;
394 else if (build_type == BT_SHIP)
395 return design_id < rhs.design_id;
396
397 return false;
398 }
399
400 std::map<std::string, std::map<int, float>>
CompletionSpecialConsumption(int location_id) const401 ProductionQueue::ProductionItem::CompletionSpecialConsumption(int location_id) const {
402 std::map<std::string, std::map<int, float>> retval;
403
404 switch (build_type) {
405 case BT_BUILDING: {
406 if (const BuildingType* bt = GetBuildingType(name)) {
407 auto location_obj = Objects().get(location_id);
408 ScriptingContext context(location_obj);
409
410 for (const auto& psc : bt->ProductionSpecialConsumption()) {
411 if (!psc.second.first)
412 continue;
413 Condition::ObjectSet matches;
414 // if a condition selectin gwhere to take resources from was specified, use it.
415 // Otherwise take from the production location
416 if (psc.second.second) {
417 psc.second.second->Eval(context, matches);
418 } else {
419 matches.push_back(location_obj);
420 }
421
422 // determine how much to take from each matched object
423 for (auto& object : matches) {
424 context.effect_target = std::const_pointer_cast<UniverseObject>(object);
425 retval[psc.first][object->ID()] += psc.second.first->Eval(context);
426 }
427 }
428 }
429 break;
430 }
431 case BT_SHIP: {
432 if (const ShipDesign* sd = GetShipDesign(design_id)) {
433 auto location_obj = Objects().get(location_id);
434 ScriptingContext context(location_obj);
435
436 if (const ShipHull* ship_hull = GetShipHull(sd->Hull())) {
437 for (const auto& psc : ship_hull->ProductionSpecialConsumption()) {
438 if (!psc.second.first)
439 continue;
440 retval[psc.first][location_id] += psc.second.first->Eval(context);
441 }
442 }
443
444 for (const std::string& part_name : sd->Parts()) {
445 const ShipPart* part = GetShipPart(part_name);
446 if (!part)
447 continue;
448 for (const auto& psc : part->ProductionSpecialConsumption()) {
449 if (!psc.second.first)
450 continue;
451 retval[psc.first][location_id] += psc.second.first->Eval(context);
452 }
453 }
454 }
455 break;
456 }
457 case BT_PROJECT: // TODO
458 case BT_STOCKPILE: // stockpile transfer consumes no special
459 default:
460 break;
461 }
462
463 return retval;
464 }
465
466 std::map<MeterType, std::map<int, float>>
CompletionMeterConsumption(int location_id) const467 ProductionQueue::ProductionItem::CompletionMeterConsumption(int location_id) const {
468 std::map<MeterType, std::map<int, float>> retval;
469
470 switch (build_type) {
471 case BT_BUILDING: {
472 if (const BuildingType* bt = GetBuildingType(name)) {
473 auto obj = Objects().get(location_id);
474 ScriptingContext context(obj);
475
476 for (const auto& pmc : bt->ProductionMeterConsumption()) {
477 if (!pmc.second.first)
478 continue;
479 retval[pmc.first][location_id] = pmc.second.first->Eval(context);
480 }
481 }
482 break;
483 }
484 case BT_SHIP: {
485 if (const ShipDesign* sd = GetShipDesign(design_id)) {
486 auto obj = Objects().get(location_id);
487 ScriptingContext context(obj);
488
489 if (const ShipHull* ship_hull = GetShipHull(sd->Hull())) {
490 for (const auto& pmc : ship_hull->ProductionMeterConsumption()) {
491 if (!pmc.second.first)
492 continue;
493 retval[pmc.first][location_id] += pmc.second.first->Eval(context);
494 }
495 }
496
497 for (const std::string& part_name : sd->Parts()) {
498 const ShipPart* pt = GetShipPart(part_name);
499 if (!pt)
500 continue;
501 for (const auto& pmc : pt->ProductionMeterConsumption()) {
502 if (!pmc.second.first)
503 continue;
504 retval[pmc.first][location_id] += pmc.second.first->Eval(context);
505 }
506 }
507 }
508 break;
509 }
510 case BT_PROJECT: // TODO
511 case BT_STOCKPILE: // stockpile transfer happens before completion - nothing to do
512 default:
513 break;
514 }
515
516 return retval;
517 }
518
Dump() const519 std::string ProductionQueue::ProductionItem::Dump() const {
520 std::string retval = "ProductionItem: " + boost::lexical_cast<std::string>(build_type);
521 if (!name.empty())
522 retval += " name: " + name;
523 if (design_id != INVALID_DESIGN_ID)
524 retval += " id: " + std::to_string(design_id);
525 return retval;
526 }
527
528
529 //////////////////////////////
530 // ProductionQueue::Element //
531 //////////////////////////////
Element()532 ProductionQueue::Element::Element() :
533 uuid(boost::uuids::nil_generator()())
534 {}
535
Element(ProductionItem item_,int empire_id_,boost::uuids::uuid uuid_,int ordered_,int remaining_,int blocksize_,int location_,bool paused_,bool allowed_imperial_stockpile_use_)536 ProductionQueue::Element::Element(ProductionItem item_, int empire_id_,
537 boost::uuids::uuid uuid_, int ordered_,
538 int remaining_, int blocksize_, int location_, bool paused_,
539 bool allowed_imperial_stockpile_use_) :
540 item(item_),
541 empire_id(empire_id_),
542 ordered(ordered_),
543 blocksize(blocksize_),
544 remaining(remaining_),
545 location(location_),
546 blocksize_memory(blocksize_),
547 paused(paused_),
548 allowed_imperial_stockpile_use(allowed_imperial_stockpile_use_),
549 uuid(uuid_)
550 {}
551
Element(BuildType build_type,std::string name,int empire_id_,boost::uuids::uuid uuid_,int ordered_,int remaining_,int blocksize_,int location_,bool paused_,bool allowed_imperial_stockpile_use_)552 ProductionQueue::Element::Element(BuildType build_type, std::string name, int empire_id_,
553 boost::uuids::uuid uuid_, int ordered_,
554 int remaining_, int blocksize_, int location_, bool paused_,
555 bool allowed_imperial_stockpile_use_) :
556 item(build_type, name),
557 empire_id(empire_id_),
558 ordered(ordered_),
559 blocksize(blocksize_),
560 remaining(remaining_),
561 location(location_),
562 blocksize_memory(blocksize_),
563 paused(paused_),
564 allowed_imperial_stockpile_use(allowed_imperial_stockpile_use_),
565 uuid(uuid_)
566 {}
567
Element(BuildType build_type,int design_id,int empire_id_,boost::uuids::uuid uuid_,int ordered_,int remaining_,int blocksize_,int location_,bool paused_,bool allowed_imperial_stockpile_use_)568 ProductionQueue::Element::Element(BuildType build_type, int design_id, int empire_id_,
569 boost::uuids::uuid uuid_, int ordered_,
570 int remaining_, int blocksize_, int location_, bool paused_,
571 bool allowed_imperial_stockpile_use_) :
572 item(build_type, design_id),
573 empire_id(empire_id_),
574 ordered(ordered_),
575 blocksize(blocksize_),
576 remaining(remaining_),
577 location(location_),
578 blocksize_memory(blocksize_),
579 paused(paused_),
580 allowed_imperial_stockpile_use(allowed_imperial_stockpile_use_),
581 uuid(uuid_)
582 {}
583
Dump() const584 std::string ProductionQueue::Element::Dump() const {
585 std::string retval = "ProductionQueue::Element (" + item.Dump() + ") (" +
586 std::to_string(blocksize) + ") x" + std::to_string(ordered) + " ";
587 retval += " (remaining: " + std::to_string(remaining) + ") uuid: " + boost::uuids::to_string(uuid);
588 return retval;
589 }
590
591
592 /////////////////////
593 // ProductionQueue //
594 /////////////////////
ProductionQueue(int empire_id)595 ProductionQueue::ProductionQueue(int empire_id) :
596 m_empire_id(empire_id)
597 {}
598
ProjectsInProgress() const599 int ProductionQueue::ProjectsInProgress() const
600 { return m_projects_in_progress; }
601
TotalPPsSpent() const602 float ProductionQueue::TotalPPsSpent() const {
603 // add up allocated PP from all resource sharing object groups
604 float retval = 0.0f;
605 for (const auto& entry : m_object_group_allocated_pp)
606 { retval += entry.second; }
607 return retval;
608 }
609
610 /** TODO: Is there any reason to keep this method in addition to the more
611 * specific information directly available from the empire? This should
612 * probably at least be renamed to clarify it is non-stockpile output */
AvailablePP(const std::shared_ptr<ResourcePool> & industry_pool) const613 std::map<std::set<int>, float> ProductionQueue::AvailablePP(
614 const std::shared_ptr<ResourcePool>& industry_pool) const
615 {
616 std::map<std::set<int>, float> retval;
617 if (!industry_pool) {
618 ErrorLogger() << "ProductionQueue::AvailablePP passed invalid industry resource pool";
619 return retval;
620 }
621
622 // determine available PP (ie. industry) in each resource sharing group of systems
623 for (const auto& ind : industry_pool->Output()) { // get group of systems in industry pool
624 const std::set<int>& group = ind.first;
625 retval[group] = ind.second;
626 }
627
628 return retval;
629 }
630
AllocatedPP() const631 const std::map<std::set<int>, float>& ProductionQueue::AllocatedPP() const
632 { return m_object_group_allocated_pp; }
633
AllocatedStockpilePP() const634 const std::map<std::set<int>, float>& ProductionQueue::AllocatedStockpilePP() const
635 { return m_object_group_allocated_stockpile_pp; }
636
StockpileCapacity() const637 float ProductionQueue::StockpileCapacity() const {
638 if (m_empire_id == ALL_EMPIRES)
639 return 0.0f;
640
641 float retval = 0.0f;
642
643 for (const auto& obj : Objects().ExistingObjects()) {
644 if (!obj.second->OwnedBy(m_empire_id))
645 continue;
646 const auto* meter = obj.second->GetMeter(METER_STOCKPILE);
647 if (!meter)
648 continue;
649 retval += meter->Current();
650 }
651 return retval;
652 }
653
ObjectsWithWastedPP(const std::shared_ptr<ResourcePool> & industry_pool) const654 std::set<std::set<int>> ProductionQueue::ObjectsWithWastedPP(
655 const std::shared_ptr<ResourcePool>& industry_pool) const
656 {
657 std::set<std::set<int>> retval;
658 if (!industry_pool) {
659 ErrorLogger() << "ProductionQueue::ObjectsWithWastedPP passed invalid industry resource pool";
660 return retval;
661 }
662
663 for (auto& avail_pp : AvailablePP(industry_pool)) {
664 //std::cout << "available PP groups size: " << avail_pp.first.size() << " pp: " << avail_pp.second << std::endl;
665
666 if (avail_pp.second <= 0)
667 continue; // can't waste if group has no PP
668 const std::set<int>& group = avail_pp.first;
669 // find this group's allocated PP
670 auto alloc_it = m_object_group_allocated_pp.find(group);
671 // is less allocated than is available? if so, some is wasted (assumes stockpile contribuutions can never be lossless)
672 // XXX maybe should check stockpile input ratio
673 if (alloc_it == m_object_group_allocated_pp.end() || alloc_it->second < avail_pp.second)
674 retval.insert(avail_pp.first);
675 }
676 return retval;
677 }
678
empty() const679 bool ProductionQueue::empty() const
680 { return !m_queue.size(); }
681
size() const682 unsigned int ProductionQueue::size() const
683 { return m_queue.size(); }
684
begin() const685 ProductionQueue::const_iterator ProductionQueue::begin() const
686 { return m_queue.begin(); }
687
end() const688 ProductionQueue::const_iterator ProductionQueue::end() const
689 { return m_queue.end(); }
690
find(int i) const691 ProductionQueue::const_iterator ProductionQueue::find(int i) const
692 { return (0 <= i && i < static_cast<int>(size())) ? (begin() + i) : end(); }
693
operator [](int i) const694 const ProductionQueue::Element& ProductionQueue::operator[](int i) const {
695 if (i < 0 || i >= static_cast<int>(m_queue.size()))
696 throw std::out_of_range("Tried to access ProductionQueue element out of bounds");
697 return m_queue[i];
698 }
699
find(boost::uuids::uuid uuid) const700 ProductionQueue::const_iterator ProductionQueue::find(boost::uuids::uuid uuid) const {
701 if (uuid == boost::uuids::nil_generator()())
702 return m_queue.end();
703 for (auto it = m_queue.begin(); it != m_queue.end(); ++it)
704 if (it->uuid == uuid)
705 return it;
706 return m_queue.end();
707 }
708
IndexOfUUID(boost::uuids::uuid uuid) const709 int ProductionQueue::IndexOfUUID(boost::uuids::uuid uuid) const {
710 auto it = find(uuid);
711 if (it == end())
712 return -1;
713 return std::distance(begin(), it);
714 }
715
Update()716 void ProductionQueue::Update() {
717 const Empire* empire = GetEmpire(m_empire_id);
718 if (!empire) {
719 ErrorLogger() << "ProductionQueue::Update passed null empire. doing nothing.";
720 m_projects_in_progress = 0;
721 m_object_group_allocated_pp.clear();
722 return;
723 }
724
725 ScopedTimer update_timer("ProductionQueue::Update");
726
727 auto industry_resource_pool = empire->GetResourcePool(RE_INDUSTRY);
728 auto available_pp = AvailablePP(industry_resource_pool);
729 float pp_in_stockpile = industry_resource_pool->Stockpile();
730 TraceLogger() << "========= pp_in_stockpile: " << pp_in_stockpile << " ========";
731 float stockpile_limit = StockpileCapacity();
732 float available_stockpile = std::min(pp_in_stockpile, stockpile_limit);
733 TraceLogger() << "========= available_stockpile: " << available_stockpile << " ========";
734
735 // determine which resource sharing group each queue item is located in
736 std::vector<std::set<int>> queue_element_groups;
737 for (const auto& element : m_queue) {
738 // get location object for element
739 int location_id = element.location;
740
741 // search through groups to find object
742 for (auto groups_it = available_pp.begin();
743 true; ++groups_it)
744 {
745 if (groups_it == available_pp.end()) {
746 // didn't find a group containing this object, so add an empty group as this element's queue element group
747 queue_element_groups.push_back(std::set<int>());
748 break;
749 }
750
751 // check if location object id is in this group
752 const auto& group = groups_it->first;
753 auto set_it = group.find(location_id);
754 if (set_it != group.end()) {
755 // system is in this group.
756 queue_element_groups.push_back(group); // record this discovery
757 break; // stop searching for a group containing a system, since one has been found
758 }
759 }
760 }
761
762 // cache producibility, and production item costs and times
763 // initialize production queue item completion status to 'never'
764 std::map<std::pair<ProductionQueue::ProductionItem, int>,
765 std::pair<float, int>> queue_item_costs_and_times;
766 std::vector<bool> is_producible;
767 for (auto& elem : m_queue) {
768 is_producible.push_back(empire->ProducibleItem(elem.item, elem.location));
769 // for items that don't depend on location, only store cost/time once
770 int location_id = (elem.item.CostIsProductionLocationInvariant() ? INVALID_OBJECT_ID : elem.location);
771 std::pair<ProductionQueue::ProductionItem, int> key(elem.item, location_id);
772
773 if (!queue_item_costs_and_times.count(key))
774 queue_item_costs_and_times[key] = empire->ProductionCostAndTime(elem);
775
776 elem.turns_left_to_next_item = -1;
777 elem.turns_left_to_completion = -1;
778 }
779
780 // duplicate production queue state for future simulation
781 QueueType sim_queue = m_queue;
782 std::vector<unsigned int> sim_queue_original_indices(sim_queue.size());
783 for (unsigned int i = 0; i < sim_queue_original_indices.size(); ++i)
784 sim_queue_original_indices[i] = i;
785
786 // allocate pp to queue elements, returning updated available pp and updated
787 // allocated pp for each group of resource sharing objects
788 float project_transfer_to_stockpile = SetProdQueueElementSpending(
789 available_pp, available_stockpile, stockpile_limit, queue_element_groups,
790 queue_item_costs_and_times, is_producible, m_queue,
791 m_object_group_allocated_pp, m_object_group_allocated_stockpile_pp,
792 m_projects_in_progress, false);
793
794 //update expected new stockpile amount
795 m_expected_new_stockpile_amount = CalculateNewStockpile(
796 m_empire_id, pp_in_stockpile, project_transfer_to_stockpile, available_pp, m_object_group_allocated_pp,
797 m_object_group_allocated_stockpile_pp);
798 m_expected_project_transfer_to_stockpile = project_transfer_to_stockpile;
799
800 // if at least one resource-sharing system group have available PP, simulate
801 // future turns to predict when build items will be finished
802 bool simulate_future = false;
803 for (auto& available : available_pp) {
804 if (available.second > EPSILON) {
805 simulate_future = true;
806 break;
807 }
808 }
809
810 if (!simulate_future) {
811 DebugLogger() << "not enough PP to be worth simulating future turns production. marking everything as never complete";
812 ProductionQueueChangedSignal();
813 return;
814 }
815
816 // there are enough PP available in at least one group to make it worthwhile to simulate the future.
817 DebugLogger() << "ProductionQueue::Update: Simulating future turns of production queue";
818
819 const int TOO_MANY_TURNS = 500; // stop counting turns to completion after this long, to prevent seemingly endless loops
820 const float TOO_LONG_TIME = 0.5f; // max time in seconds to spend simulating queue
821
822
823 // remove from simulated queue any paused items and items that can't be built due to not
824 // meeting their location conditions; can't feasibly re-check
825 // buildability each projected turn as this would require creating a simulated
826 // universe into which simulated completed buildings could be inserted, as
827 // well as spoofing the current turn, or otherwise faking the results for
828 // evaluating arbitrary location conditions for the simulated universe.
829 // this would also be inaccurate anyway due to player choices or random
830 // chance, so for simplicity, it is assumed that building location
831 // conditions evaluated at the present turn apply indefinitely.
832 //
833 for (unsigned int i = 0; i < sim_queue.size(); ++i) {
834 if (sim_queue[i].paused || !is_producible[i]) {
835 sim_queue.erase(sim_queue.begin() + i);
836 is_producible.erase(is_producible.begin() + i);
837 queue_element_groups.erase(queue_element_groups.begin() + i);
838 sim_queue_original_indices.erase(sim_queue_original_indices.begin() + i--);
839 }
840 }
841
842 boost::posix_time::ptime sim_time_start;
843 boost::posix_time::ptime sim_time_end;
844 long sim_time;
845 sim_time_start = boost::posix_time::ptime(boost::posix_time::microsec_clock::local_time());
846 std::map<std::set<int>, float> allocated_pp;
847 float sim_available_stockpile = available_stockpile;
848 float sim_pp_in_stockpile = pp_in_stockpile;
849 std::map<std::set<int>, float> allocated_stockpile_pp;
850 int dummy_int = 0;
851
852 for (int sim_turn = 1; sim_turn <= TOO_MANY_TURNS; sim_turn ++) {
853 long sim_time_until_now = (boost::posix_time::ptime(boost::posix_time::microsec_clock::local_time()) - sim_time_start).total_microseconds();
854 if ((sim_time_until_now * 1e-6) >= TOO_LONG_TIME)
855 break;
856
857 TraceLogger() << "sim turn: " << sim_turn << " sim queue size: " << sim_queue.size();
858 if (sim_queue.empty() && sim_turn > 2)
859 break;
860
861 allocated_pp.clear();
862 allocated_stockpile_pp.clear();
863
864 float sim_project_transfer_to_stockpile = SetProdQueueElementSpending(
865 available_pp, sim_available_stockpile, stockpile_limit, queue_element_groups,
866 queue_item_costs_and_times, is_producible, sim_queue,
867 allocated_pp, allocated_stockpile_pp, dummy_int, true);
868
869 // check completion status and update m_queue and sim_queue as appropriate
870 for (unsigned int i = 0; i < sim_queue.size(); i++) {
871 ProductionQueue::Element& sim_element = sim_queue[i];
872 ProductionQueue::Element& orig_element = m_queue[sim_queue_original_indices[i]];
873 if (sim_element.turns_left_to_next_item != 1)
874 continue;
875 sim_element.progress = std::max(0.0f, sim_element.progress - 1.0f);
876 if (orig_element.turns_left_to_next_item == -1)
877 orig_element.turns_left_to_next_item = sim_turn;
878 sim_element.turns_left_to_next_item = -1;
879
880 // if all repeats of item are complete, update completion time and remove from sim_queue
881 if (--sim_element.remaining == 0) {
882 orig_element.turns_left_to_completion = sim_turn;
883 sim_queue.erase(sim_queue.begin() + i);
884 is_producible.erase(is_producible.begin() + i);
885 queue_element_groups.erase(queue_element_groups.begin() + i);
886 sim_queue_original_indices.erase(sim_queue_original_indices.begin() + i--);
887 }
888 }
889 sim_pp_in_stockpile = CalculateNewStockpile(
890 m_empire_id, sim_pp_in_stockpile, sim_project_transfer_to_stockpile,
891 available_pp, allocated_pp, allocated_stockpile_pp);
892 sim_available_stockpile = std::min(sim_pp_in_stockpile, stockpile_limit);
893 }
894
895 sim_time_end = boost::posix_time::ptime(boost::posix_time::microsec_clock::local_time());
896 sim_time = (sim_time_end - sim_time_start).total_microseconds();
897 if ((sim_time * 1e-6) >= TOO_LONG_TIME) {
898 DebugLogger() << "ProductionQueue::Update: Projections timed out after " << sim_time
899 << " microseconds; all remaining items in queue marked completing 'Never'.";
900 }
901 DebugLogger() << "ProductionQueue::Update: Projections took "
902 << ((sim_time_end - sim_time_start).total_microseconds()) << " microseconds with "
903 << empire->ResourceOutput(RE_INDUSTRY) << " industry output";
904 ProductionQueueChangedSignal();
905 }
906
push_back(const Element & element)907 void ProductionQueue::push_back(const Element& element) {
908 if (find(element.uuid) != end()) {
909 ErrorLogger() << "Trying to push back repeated UUID " << element.uuid;
910 throw std::invalid_argument("Repeated use of UUID");
911 }
912 m_queue.push_back(element);
913 }
914
insert(iterator it,const Element & element)915 void ProductionQueue::insert(iterator it, const Element& element) {
916 if (find(element.uuid) != end()) {
917 ErrorLogger() << "Trying to insert repeated UUID " << element.uuid;
918 throw std::invalid_argument("Repeated use of UUID");
919 }
920 m_queue.insert(it, element);
921 }
922
erase(int i)923 void ProductionQueue::erase(int i) {
924 if (i < 0 || i >= static_cast<int>(m_queue.size()))
925 throw std::out_of_range("Tried to erase ProductionQueue item out of bounds.");
926 m_queue.erase(begin() + i);
927 }
928
erase(iterator it)929 ProductionQueue::iterator ProductionQueue::erase(iterator it) {
930 if (it == end())
931 throw std::out_of_range("Tried to erase ProductionQueue item out of bounds.");
932 return m_queue.erase(it);
933 }
934
begin()935 ProductionQueue::iterator ProductionQueue::begin()
936 { return m_queue.begin(); }
937
end()938 ProductionQueue::iterator ProductionQueue::end()
939 { return m_queue.end(); }
940
find(int i)941 ProductionQueue::iterator ProductionQueue::find(int i)
942 { return (0 <= i && i < static_cast<int>(size())) ? (begin() + i) : end(); }
943
operator [](int i)944 ProductionQueue::Element& ProductionQueue::operator[](int i) {
945 if (i < 0 || i >= static_cast<int>(m_queue.size()))
946 throw std::out_of_range("Tried to access ProductionQueue element out of bounds");
947 return m_queue[i];
948 }
949
clear()950 void ProductionQueue::clear() {
951 m_queue.clear();
952 m_projects_in_progress = 0;
953 m_object_group_allocated_pp.clear();
954 ProductionQueueChangedSignal();
955 }
956