1 #include "CGroup.h"
2
3 #include <sstream>
4 #include <iostream>
5 #include <string>
6
7 #include "CAI.h"
8 #include "CUnit.h"
9 #include "CUnitTable.h"
10 #include "CTaskHandler.h"
11 #include "CPathfinder.h"
12 #include "CDefenseMatrix.h"
13
14 int CGroup::counter = 0;
15
addUnit(CUnit & unit)16 void CGroup::addUnit(CUnit &unit) {
17 if (unit.group) {
18 if (unit.group == this) {
19 LOG_WW("CGroup::addUnit " << unit << " already registered in " << (*this))
20 return; // already registered
21 } else {
22 // NOTE: unit can exist in one and only group
23 //LOG_II("CGroup::addUnit " << unit << " still in " << (*(unit.group)))
24 unit.group->remove(unit);
25 }
26 }
27
28 assert(unit.group == NULL);
29
30 units[unit.key] = &unit;
31 unit.reg(*this);
32 unit.group = this;
33
34 recalcProperties(&unit);
35
36 LOG_II("CGroup::addUnit " << unit << " to " << (*this))
37 // TODO: if group is busy invoke new unit to community process?
38 }
39
remove()40 void CGroup::remove() {
41 LOG_II("CGroup::remove " << (*this))
42
43 // NOTE: removal order below is important
44
45 std::list<ARegistrar*>::iterator j = records.begin();
46 while(j != records.end()) {
47 ARegistrar *regobj = *j; ++j;
48 // remove from CEconomy, CPathfinder, ATask, CTaskHandler
49 regobj->remove(*this);
50 }
51
52 std::map<int, CUnit*>::iterator i;
53 for (i = units.begin(); i != units.end(); i++) {
54 i->second->unreg(*this);
55 i->second->group = NULL;
56 }
57 units.clear();
58 badTargets.clear();
59
60 assert(records.empty());
61 }
62
remove(ARegistrar & object)63 void CGroup::remove(ARegistrar &object) {
64 CUnit *unit = dynamic_cast<CUnit*>(&object);
65
66 LOG_II("CGroup::remove " << (*unit) << " from " << (*this))
67
68 assert(units.find(unit->key) != units.end());
69
70 unit->group = NULL;
71 unit->unreg(*this);
72 units.erase(unit->key);
73
74 if (unit == latecomerUnit)
75 removeLatecomer();
76
77 badTargets.clear();
78
79 /* If no more units remain in this group, remove the group */
80 if (units.empty()) {
81 remove();
82 } else {
83 /* Recalculate properties of the current group */
84 recalcProperties(NULL, true);
85
86 std::map<int, CUnit*>::iterator i;
87 for (i = units.begin(); i != units.end(); ++i) {
88 recalcProperties(i->second);
89 }
90 }
91 }
92
reclaim(int entity,bool feature)93 void CGroup::reclaim(int entity, bool feature) {
94 float3 pos;
95
96 if (feature) {
97 pos = ai->cb->GetFeaturePos(entity);
98 if (pos == ZeroVector)
99 return;
100 }
101
102 std::map<int, CUnit*>::iterator i;
103 for (i = units.begin(); i != units.end(); ++i) {
104 if (i->second->def->canReclaim) {
105 if (feature)
106 i->second->reclaim(pos, 16.0f);
107 else
108 i->second->reclaim(entity);
109 }
110 }
111 }
112
repair(int target)113 void CGroup::repair(int target) {
114 std::map<int, CUnit*>::iterator i;
115 for (i = units.begin(); i != units.end(); ++i) {
116 if (i->second->def->canRepair)
117 i->second->repair(target);
118 }
119 }
120
abilities(bool on)121 void CGroup::abilities(bool on) {
122 std::map<int, CUnit*>::iterator i;
123 for (i = units.begin(); i != units.end(); ++i) {
124 if (i->second->def->canCloak)
125 i->second->cloak(on);
126 }
127 }
128
micro(bool on)129 void CGroup::micro(bool on) {
130 std::map<int, CUnit*>::iterator i;
131 for (i = units.begin(); i != units.end(); ++i)
132 i->second->micro(on);
133 }
134
isMicroing()135 bool CGroup::isMicroing() {
136 std::map<int, CUnit*>::iterator i;
137 for (i = units.begin(); i != units.end(); ++i) {
138 if (i->second->isMicroing())
139 return true;
140 }
141 return false;
142 }
143
isIdle()144 bool CGroup::isIdle() {
145 bool idle = true;
146 std::map<int, CUnit*>::iterator i;
147 for (i = units.begin(); i != units.end(); ++i) {
148 if (!ai->unittable->idle[i->second->key]) {
149 idle = false;
150 break;
151 }
152 }
153 return idle;
154 }
155
reset()156 void CGroup::reset() {
157 LOG_II("CGroup::reset " << (*this))
158
159 assert(units.empty());
160 recalcProperties(NULL, true);
161 busy = false;
162 micro(false);
163 abilities(false);
164 units.clear();
165 records.clear();
166 badTargets.clear();
167 }
168
recalcProperties(CUnit * unit,bool reset)169 void CGroup::recalcProperties(CUnit* unit, bool reset)
170 {
171 if (reset) {
172 strength = 0.0f;
173 speed = std::numeric_limits<float>::max();
174 size = 0;
175 buildSpeed = 0.0f;
176 range = 0.0f;
177 buildRange = 0.0f;
178 los = 0.0f;
179 maxSlope = 1.0f;
180 pathType = -1; // emulate NONE
181 techlvl = TECH1;
182 cats.reset();
183 groupRadius = 0.0f;
184 radiusUpdateRequired = false;
185 cost = 0.0f;
186 costMetal = 0.0f;
187 worstSpeedUnit = NULL;
188 worstSlopeUnit = NULL;
189 latecomerUnit = NULL;
190 latecomerWeight = 0;
191 }
192
193 if (unit == NULL)
194 return;
195
196 if (unit->builtBy >= 0) {
197 for (int i = MIN_TECHLEVEL - 1; i < MAX_TECHLEVEL; i++) {
198 if (unit->techlvl[i]) {
199 for (int j = MIN_TECHLEVEL - 1; i < MAX_TECHLEVEL; j++) {
200 if (techlvl[j]) {
201 if (i > j) {
202 techlvl.reset();
203 techlvl.set(i);
204 }
205 }
206 break;
207 }
208 break;
209 }
210 }
211 }
212
213 mergeCats(unit->type->cats);
214
215 // NOTE: aircraft & static units do not have movedata
216 MoveData* md = unit->def->movedata;
217 if (md) {
218 bool validCandidate = true;
219
220 if (units.size() > 1) {
221 unitCategory
222 envCatsOfGroup = CATS_ENV&cats,
223 envCatsOfUnit = CATS_ENV&unit->type->cats;
224 validCandidate = (envCatsOfGroup == envCatsOfUnit);
225 }
226
227 if (validCandidate) {
228 // select base path type with the lowerest slope, so pos(true) will
229 // return valid postition for all units in a group...
230 if (md->maxSlope <= maxSlope) {
231 pathType = md->pathType;
232 maxSlope = md->maxSlope;
233 worstSlopeUnit = unit;
234 }
235 }
236 }
237
238 strength += unit->type->dps;
239 buildSpeed += unit->def->buildSpeed;
240 size += FOOTPRINT2REAL * std::max<int>(unit->def->xsize, unit->def->zsize);
241 range = std::max<float>(ai->cb->GetUnitMaxRange(unit->key), range);
242 buildRange = std::max<float>(unit->def->buildDistance, buildRange);
243 los = std::max<float>(unit->def->losRadius, los);
244 cost += unit->type->cost;
245 costMetal += unit->type->costMetal;
246
247 float temp;
248 temp = ai->cb->GetUnitSpeed(unit->key);
249 if (temp < speed) {
250 speed = temp;
251 worstSpeedUnit = unit;
252 }
253
254 radiusUpdateRequired = true;
255 }
256
merge(CGroup & group)257 void CGroup::merge(CGroup &group) {
258 std::map<int, CUnit*>::iterator i = group.units.begin();
259 // NOTE: "group" will automatically be removed when last unit
260 // is transferred
261 while(i != group.units.end()) {
262 CUnit *unit = i->second; ++i;
263 assert(unit->group == &group);
264 addUnit(*unit);
265 }
266 }
267
pos(bool force_valid)268 float3 CGroup::pos(bool force_valid) {
269 std::map<int, CUnit*>::iterator i;
270 float3 pos(0.0f, 0.0f, 0.0f);
271
272 for (i = units.begin(); i != units.end(); ++i)
273 pos += ai->cb->GetUnitPos(i->first);
274
275 pos /= units.size();
276
277 if (force_valid) {
278 if (ai->pathfinder->isBlocked(pos.x, pos.z, pathType)) {
279 float3 posValid = ai->pathfinder->getClosestPos(pos, PATH2REAL, this);
280 if (posValid == ERRORVECTOR) {
281 float bestDistance = std::numeric_limits<float>::max();
282 for (i = units.begin(); i != units.end(); ++i) {
283 float3 pos2 = ai->cb->GetUnitPos(i->first);
284 if (ai->pathfinder->isBlocked(pos2.x, pos2.z, pathType))
285 pos2 = ai->pathfinder->getClosestPos(pos2, PATH2REAL, this);
286 if (pos2 != ERRORVECTOR) {
287 float distance = pos.distance2D(pos2);
288 if (distance < bestDistance) {
289 posValid = pos2;
290 bestDistance = distance;
291 }
292 }
293
294 }
295 }
296 return posValid;
297 }
298 }
299
300 return pos;
301 }
302
radius()303 float CGroup::radius() {
304 if (radiusUpdateRequired) {
305 int i;
306 // get number of units per leg length in a square
307 for(i = 1; units.size() > i * i; i++);
308 // calculate length of leg of square
309 float sqLeg = maxLength() * i / (float)units.size();
310 sqLeg *= sqLeg;
311 // calculate half of hypotenuse
312 groupRadius = sqrt(sqLeg + sqLeg) / 2.0f;
313
314 radiusUpdateRequired = false;
315 }
316 return groupRadius;
317 }
318
maxLength()319 int CGroup::maxLength() {
320 return size + units.size() * FOOTPRINT2REAL;
321 }
322
assist(ATask & t)323 void CGroup::assist(ATask &t) {
324 // t->addAssister(
325 switch(t.t) {
326 case TASK_BUILD: {
327 BuildTask *task = dynamic_cast<BuildTask*>(&t);
328 CUnit *unit = task->firstGroup()->firstUnit();
329 guard(unit->key);
330 break;
331 }
332
333 case TASK_ATTACK: {
334 // TODO: Calculate the flanking pos and attack from there
335 AttackTask *task = dynamic_cast<AttackTask*>(&t);
336 attack(task->target);
337 break;
338 }
339
340 case TASK_FACTORY: {
341 FactoryTask *task = dynamic_cast<FactoryTask*>(&t);
342 CUnit *unit = task->firstGroup()->firstUnit();
343 guard(unit->key);
344 break;
345 }
346
347 default: return;
348 }
349 }
350
move(float3 & pos,bool enqueue)351 void CGroup::move(float3 &pos, bool enqueue) {
352 std::map<int, CUnit*>::iterator i;
353 for (i = units.begin(); i != units.end(); ++i)
354 i->second->move(pos, enqueue);
355 }
356
wait()357 void CGroup::wait() {
358 std::map<int, CUnit*>::iterator i;
359 for (i = units.begin(); i != units.end(); ++i)
360 i->second->wait();
361 }
362
unwait()363 void CGroup::unwait() {
364 std::map<int, CUnit*>::iterator i;
365 for (i = units.begin(); i != units.end(); ++i)
366 i->second->unwait();
367 }
368
attack(int target,bool enqueue)369 void CGroup::attack(int target, bool enqueue) {
370 std::map<int, CUnit*>::iterator i;
371 for (i = units.begin(); i != units.end(); ++i)
372 i->second->attack(target, enqueue);
373 }
374
build(float3 & pos,UnitType * ut)375 bool CGroup::build(float3& pos, UnitType* ut) {
376 std::map<int, CUnit*>::iterator alpha, i;
377 alpha = units.begin();
378 if (alpha->second->build(ut, pos)) {
379 for (i = ++alpha; i != units.end(); ++i)
380 i->second->guard(alpha->first);
381 return true;
382 }
383 return false;
384 }
385
stop()386 void CGroup::stop() {
387 std::map<int, CUnit*>::iterator i;
388 for (i = units.begin(); i != units.end(); ++i)
389 i->second->stop();
390 }
391
guard(int target,bool enqueue)392 void CGroup::guard(int target, bool enqueue) {
393 std::map<int, CUnit*>::iterator i;
394 for (i = units.begin(); i != units.end(); ++i)
395 i->second->guard(target, enqueue);
396 }
397
canTouch(const float3 & goal)398 bool CGroup::canTouch(const float3 &goal) {
399 return !ai->pathfinder->isBlocked(goal.x, goal.z, pathType);
400 }
401
canReach(const float3 & goal)402 bool CGroup::canReach(const float3 &goal) {
403 if (!canTouch(goal))
404 return false;
405 if (pathType < 0)
406 return true;
407
408 float3 gpos = pos(true);
409
410 return ai->pathfinder->pathExists(*this, gpos, goal);
411 }
412
canAttack(int uid)413 bool CGroup::canAttack(int uid) {
414 if ((cats&ATTACKER).none() && !firstUnit()->def->canReclaim)
415 return false;
416
417 const UnitDef *ud = ai->cbc->GetUnitDef(uid);
418
419 if (ud == NULL || ai->cbc->IsUnitCloaked(uid))
420 return false;
421
422 const unitCategory ecats = UC(ud->id);
423 float3 epos = ai->cbc->GetUnitPos(uid);
424
425 if ((ecats&AIR).any() && (cats&ANTIAIR).none())
426 return false;
427
428 const float enemyRadiusDiv2 = ai->cb->GetUnitDefRadius(ud->id) / 2.0f;
429 bool underwater = (epos.y + enemyRadiusDiv2) < 0.0f;
430 bool outofwater = (epos.y - enemyRadiusDiv2) >= 0.0f;
431
432 if (underwater && (cats&TORPEDO).none())
433 return false;
434 if (outofwater && (CATS_ENV&cats) == SUB)
435 return false;
436
437 // TODO: add more tweaks based on physical weapon possibilities
438
439 return true;
440 }
441
canAdd(CUnit * unit)442 bool CGroup::canAdd(CUnit* unit) {
443 return canBeMerged(cats, unit->type->cats);
444 }
445
canAssist(UnitType * type)446 bool CGroup::canAssist(UnitType* type) {
447 if (type) {
448 if (!type->def->canBeAssisted)
449 return false;
450 if ((type->cats&(SEA|SUB)).any() && (cats&(SEA|SUB|AIR)).none())
451 return false;
452 if ((type->cats&(LAND)).any() && (cats&(LAND|AIR)).none())
453 return false;
454 }
455
456 std::map<int, CUnit*>::const_iterator i;
457 for (i = units.begin(); i != units.end(); ++i)
458 if (i->second->type->def->canAssist)
459 return true;
460
461 return false;
462 }
463
canMerge(CGroup * group)464 bool CGroup::canMerge(CGroup* group) {
465 return canBeMerged(cats, group->cats);
466 }
467
firstUnit()468 CUnit* CGroup::firstUnit() {
469 if (units.empty())
470 return NULL;
471 return units.begin()->second;
472 }
473
mergeCats(unitCategory newcats)474 void CGroup::mergeCats(unitCategory newcats) {
475 if (cats == 0)
476 cats = newcats;
477 else {
478 // NOTE: here is a list of cats which can't be left after merging
479 // if they are absent initially, i.e. LAND + LAND|SEA = LAND
480 static unitCategory nonXorCats[] = {SEA, SUB, LAND, AIR, STATIC, MOBILE, SCOUTER};
481 unitCategory oldcats = cats;
482 cats |= newcats;
483 for (int i = 0; i < sizeof(nonXorCats) / sizeof(unitCategory); i++) {
484 unitCategory tag = nonXorCats[i];
485 if ((oldcats&tag).none() && (cats&tag).any())
486 cats &= ~tag;
487 }
488 }
489 }
490
getThreat(float3 & target,float radius)491 float CGroup::getThreat(float3& target, float radius) {
492 return ai->threatmap->getThreat(target, radius, this);
493 }
494
addBadTarget(int id)495 bool CGroup::addBadTarget(int id) {
496 const UnitDef* ud = ai->cbc->GetUnitDef(id);
497 if (ud == NULL)
498 return false;
499
500 LOG_WW("CGroup::addBadTarget " << ud->humanName << "(" << id << ") to " << (*this))
501
502 const unitCategory ecats = UC(ud->id);
503 if ((ecats&STATIC).any())
504 badTargets[id] = -1;
505 else
506 badTargets[id] = ai->cb->GetCurrentFrame();
507
508 return true;
509 }
510
selectTarget(const std::map<int,UnitType * > & targets,TargetsFilter & tf)511 int CGroup::selectTarget(const std::map<int, UnitType*>& targets, TargetsFilter &tf) {
512 std::vector<int> targetsVec;
513 targetsVec.reserve(targets.size());
514 std::map<int, UnitType*>::const_iterator ti = targets.begin();
515 for (ti = targets.begin(); ti != targets.end(); ++ti) {
516 targetsVec.push_back(ti->first);
517 }
518
519 return selectTarget(targetsVec, tf);
520 }
521
selectTarget(const std::vector<int> & targets,TargetsFilter & tf)522 int CGroup::selectTarget(const std::vector<int>& targets, TargetsFilter &tf) {
523 bool scout = (cats&SCOUTER).any();
524 bool bomber = !scout && (cats&AIR).any() && (cats&ARTILLERY).any();
525 int frame = ai->cb->GetCurrentFrame();
526 float unitDamageK;
527 float3 gpos = pos();
528
529 if (targets.empty() || tf.candidatesLimit == 0)
530 return tf.bestTarget;
531
532 std::vector<int>::const_iterator ti = targets.begin();
533 for (int i = 0; ti != targets.end() && i < tf.candidatesLimit; ++ti, i++) {
534 const int t = *ti;
535
536 if (!canAttack(t) || (tf.excludeId && (*(tf.excludeId))[t]))
537 continue;
538
539 if (!badTargets.empty()) {
540 std::map<int, int>::iterator it = badTargets.find(t);
541 if (it != badTargets.end()) {
542 if (it->second < 0)
543 continue; // permanent bad target
544 if ((frame - it->second) < BAD_TARGET_TIMEOUT)
545 continue; // temporary bad target
546 else {
547 badTargets.erase(it->first);
548 LOG_II("CGroup::selectTarget bad target Unit(" << t << ") timed out for " << (*this))
549 }
550 }
551 }
552
553 const UnitDef* ud = ai->cbc->GetUnitDef(t);
554 if (ud == NULL)
555 continue;
556
557 const unitCategory ecats = UC(ud->id);
558 if ((tf.include&ecats).none() || (tf.exclude&ecats).any())
559 continue;
560
561 float3 epos = ai->cbc->GetUnitPos(t);
562 float threat = getThreat(epos, tf.threatRadius);
563 if (threat > tf.threatCeiling)
564 continue;
565
566 float unitMaxHealth = ai->cbc->GetUnitMaxHealth(t);
567 if (unitMaxHealth > EPS)
568 unitDamageK = (unitMaxHealth - ai->cbc->GetUnitHealth(t)) / unitMaxHealth;
569 else
570 unitDamageK = 0.0f;
571
572 float score = gpos.distance2D(epos);
573 score += tf.threatFactor * threat;
574 score += tf.damageFactor * unitDamageK;
575 score += tf.powerFactor * ud->power;
576
577 // TODO: refactor so params below are moved into TargetFilter?
578 if (ai->defensematrix->isPosInBounds(epos))
579 // boost in priority enemy at our base, even scout units
580 score -= 1000.0f; // TODO: better change value to the length a group can pass for 1 min (40 sec?)?
581 else if(!scout && (ecats&SCOUTER).any()) {
582 // remote scouts are not interesting for non-scout groups
583 score += 10000.0f;
584 }
585
586 if (bomber && (ecats&STATIC).any() && (ecats&ANTIAIR).any())
587 score -= 500.0f;
588
589 // do not allow land units chase after air units...
590 if ((cats&AIR).none() && (ecats&AIR).any())
591 score += 3000.0f;
592
593 // air fighters prefer aircraft...
594 if ((cats&AIR).any() && (cats&ANTIAIR).any() && (ecats&AIR).none())
595 score += 5000.0f;
596
597 if (score < tf.scoreCeiling) {
598 tf.bestTarget = t;
599 tf.scoreCeiling = score;
600 tf.threatValue = threat;
601 }
602 }
603
604 return tf.bestTarget;
605 }
606
selectTarget(float search_radius,TargetsFilter & tf)607 int CGroup::selectTarget(float search_radius, TargetsFilter &tf) {
608 int limit = std::min<int>(MAX_ENEMIES, tf.candidatesLimit);
609 int numEnemies = ai->cbc->GetEnemyUnits(&ai->unitIDs[0], pos(), search_radius, limit);
610 if (numEnemies > 0) {
611 tf.candidatesLimit = numEnemies;
612 tf.bestTarget = selectTarget(ai->unitIDs, tf);
613 }
614 return tf.bestTarget;
615 }
616
getScanRange()617 float CGroup::getScanRange() {
618 float result = radius();
619
620 if ((cats&STATIC).any())
621 result = getRange();
622 else if ((cats&BUILDER).any())
623 result += buildRange * 1.5f;
624 else if ((cats&SNIPER).any())
625 result += range * 1.05f;
626 else if ((cats&SCOUTER).any())
627 result += range * 3.0f;
628 else if ((cats&ATTACKER).any())
629 result += range * 1.4f;
630
631 return result;
632 }
633
getRange() const634 float CGroup::getRange() const {
635 if ((cats&BUILDER).any())
636 return buildRange;
637 return range;
638 }
639
canBeMerged(unitCategory bcats,unitCategory mcats)640 bool CGroup::canBeMerged(unitCategory bcats, unitCategory mcats) {
641 static unitCategory nonMergableCats[] = { AIR, LAND, ATTACKER, STATIC, MOBILE, BUILDER, SCOUTER };
642
643 if (bcats.none())
644 return true;
645
646 unitCategory c = (bcats&mcats); // common cats between two groups of cats
647
648 if (c.none() || (c&CATS_ENV).none())
649 return false;
650
651 // NOTE: if one tag of "nonMergableCats" has been disappeared after logical
652 // addition then unit or group with "mcats" can't be merged
653 for (int i = 0; i < sizeof(nonMergableCats) / sizeof(unitCategory); i++) {
654 unitCategory tag = nonMergableCats[i];
655 if ((bcats&tag).any() && (c&tag).none())
656 return false;
657 }
658
659 if ((bcats&SCOUTER).none() && (mcats&SCOUTER).any()) {
660 // merging scout group with non-scout group...
661 static unitCategory attackCats = ANTIAIR|ARTILLERY|SNIPER|ASSAULT;
662 if ((c&attackCats).none())
663 return false;
664 }
665
666 // NOTE: aircraft units have more restricted merge rules
667 // TODO: refactor with introducing Group behaviour property?
668 if ((bcats&AIR).any()) {
669 if ((bcats&ASSAULT).any() && (c&ASSAULT).none())
670 return false;
671 if ((bcats&ARTILLERY).any() && (c&ARTILLERY).none())
672 return false;
673 }
674
675 return true;
676 }
677
canPerformTasks()678 bool CGroup::canPerformTasks() {
679 std::map<int, CUnit*>::iterator i;
680 for (i = units.begin(); i != units.end(); ++i)
681 if (!i->second->canPerformTasks())
682 return false;
683 return true;
684 }
685
updateLatecomer(CUnit * unit)686 void CGroup::updateLatecomer(CUnit* unit) {
687 if (units.size() <= 1)
688 return; // to less units to register latecomer
689
690 if (latecomerUnit && latecomerUnit != unit) {
691 removeLatecomer();
692 }
693
694 if (latecomerUnit == NULL) {
695 latecomerUnit = unit;
696 latecomerPos = unit->pos();
697 return;
698 }
699
700 float3 newPos = unit->pos();
701 if (latecomerPos.distance2D(newPos) < 32.0f) {
702 latecomerWeight++;
703 if (latecomerWeight > 10) {
704 LOG_WW("CGroup::updateLatecomer " << unit << " is stuck")
705 unit->stop();
706 remove(*unit);
707 // on idle code will take unit back in action
708 ai->unittable->unitsUnderPlayerControl[unit->key] = unit;
709 }
710 }
711 else {
712 latecomerPos = newPos;
713 latecomerWeight = 0;
714 }
715 }
716
removeLatecomer()717 void CGroup::removeLatecomer() {
718 if (latecomerUnit) {
719 latecomerUnit = NULL;
720 latecomerWeight = 0;
721 }
722 }
723
operator <<(std::ostream & out,const CGroup & group)724 std::ostream& operator<<(std::ostream &out, const CGroup &group) {
725 std::stringstream ss;
726
727 ss << "Group(" << group.key
728 << "): range(" << group.getRange()
729 << "), speed(" << group.speed
730 << "), strength(" << group.strength
731 << "), amount(" << group.units.size() << ") [";
732
733 std::map<int, CUnit*>::const_iterator i = group.units.begin();
734 for (i = group.units.begin(); i != group.units.end(); ++i) {
735 ss << (*i->second) << ", ";
736 }
737 std::string s = ss.str();
738 s = s.substr(0, s.size() - 2);
739 s += "]";
740 out << s;
741
742 return out;
743 }
744