1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3
4 #include "Factory.h"
5 #include "Game/GameHelper.h"
6 #include "Game/WaitCommandsAI.h"
7 #include "Map/Ground.h"
8 #include "Map/ReadMap.h"
9 #include "Sim/Misc/GroundBlockingObjectMap.h"
10 #include "Sim/Misc/QuadField.h"
11 #include "Sim/Misc/TeamHandler.h"
12 #include "Sim/Projectiles/ProjectileHandler.h"
13 #include "Sim/Units/Scripts/UnitScript.h"
14 #include "Sim/Units/CommandAI/CommandAI.h"
15 #include "Sim/Units/CommandAI/FactoryCAI.h"
16 #include "Sim/Units/CommandAI/MobileCAI.h"
17 #include "Sim/Units/UnitHandler.h"
18 #include "Sim/Units/UnitLoader.h"
19 #include "Sim/Units/UnitDefHandler.h"
20 #include "System/EventHandler.h"
21 #include "System/Matrix44f.h"
22 #include "System/myMath.h"
23 #include "System/Sound/ISoundChannels.h"
24 #include "System/Sync/SyncTracer.h"
25
26 #include "Game/GlobalUnsynced.h"
27
28 CR_BIND_DERIVED(CFactory, CBuilding, )
29
30 CR_REG_METADATA(CFactory, (
31 CR_MEMBER(buildSpeed),
32 CR_MEMBER(opening),
33 CR_MEMBER(curBuild),
34 CR_MEMBER(nextBuildUnitDefID),
35 CR_MEMBER(lastBuildUpdateFrame),
36 CR_MEMBER(curBuildDef),
37 CR_MEMBER(curBuild),
38 //CR_MEMBER(finishedBuildFunc), FIXME is pointer
39 CR_MEMBER(finishedBuildCommand),
40 CR_MEMBER(nanoPieceCache),
41 CR_POSTLOAD(PostLoad)
42 ))
43
44 //////////////////////////////////////////////////////////////////////
45 // Construction/Destruction
46 //////////////////////////////////////////////////////////////////////
47
CFactory()48 CFactory::CFactory():
49 buildSpeed(100.0f),
50 opening(false),
51 curBuildDef(NULL),
52 curBuild(NULL),
53 nextBuildUnitDefID(-1),
54 lastBuildUpdateFrame(-1),
55 finishedBuildFunc(NULL)
56 {
57 }
58
59
60
PostLoad()61 void CFactory::PostLoad()
62 {
63 curBuildDef = unitDefHandler->GetUnitDefByID(nextBuildUnitDefID);
64
65 if (opening) {
66 script->Activate();
67 }
68 if (curBuild) {
69 script->StartBuilding();
70 }
71 }
72
KillUnit(CUnit * attacker,bool selfDestruct,bool reclaimed,bool showDeathSequence)73 void CFactory::KillUnit(CUnit* attacker, bool selfDestruct, bool reclaimed, bool showDeathSequence)
74 {
75 if (curBuild != NULL) {
76 curBuild->KillUnit(NULL, false, true);
77 curBuild = NULL;
78 }
79
80 CUnit::KillUnit(attacker, selfDestruct, reclaimed, showDeathSequence);
81 }
82
PreInit(const UnitLoadParams & params)83 void CFactory::PreInit(const UnitLoadParams& params)
84 {
85 unitDef = params.unitDef;
86 buildSpeed = unitDef->buildSpeed / TEAM_SLOWUPDATE_RATE;
87
88 CBuilding::PreInit(params);
89 }
90
91
92
CalcBuildPos(int buildPiece)93 float3 CFactory::CalcBuildPos(int buildPiece)
94 {
95 const float3 relBuildPos = script->GetPiecePos((buildPiece < 0)? script->QueryBuildInfo() : buildPiece);
96 const float3 absBuildPos = this->GetObjectSpacePos(relBuildPos);
97 return absBuildPos;
98 }
99
100
101
Update()102 void CFactory::Update()
103 {
104 nanoPieceCache.Update();
105
106 if (beingBuilt) {
107 // factory is under construction, cannot build anything yet
108 CUnit::Update();
109
110 #if 1
111 // this can happen if we started being reclaimed *while* building a
112 // unit, in which case our buildee can either be allowed to finish
113 // construction (by assisting builders) or has to be killed --> the
114 // latter is easier
115 if (curBuild != NULL) {
116 StopBuild();
117 }
118 #endif
119
120 return;
121 }
122
123
124 if (curBuildDef != NULL) {
125 if (!opening && !IsStunned()) {
126 if (groundBlockingObjectMap->CanOpenYard(this)) {
127 script->Activate();
128 groundBlockingObjectMap->OpenBlockingYard(this);
129 opening = true;
130
131 // make sure the idle-check does not immediately trigger
132 // (scripts have 7 seconds to set inBuildStance to true)
133 lastBuildUpdateFrame = gs->frameNum;
134 }
135 }
136
137 if (opening && inBuildStance && !IsStunned()) {
138 StartBuild(curBuildDef);
139 }
140 }
141
142 if (curBuild != NULL) {
143 UpdateBuild(curBuild);
144 FinishBuild(curBuild);
145 }
146
147 const bool wantClose = (!IsStunned() && opening && (gs->frameNum >= (lastBuildUpdateFrame + GAME_SPEED * 7)));
148 const bool closeYard = (wantClose && curBuild == NULL && groundBlockingObjectMap->CanCloseYard(this));
149
150 if (closeYard) {
151 // close the factory after inactivity
152 groundBlockingObjectMap->CloseBlockingYard(this);
153 opening = false;
154 script->Deactivate();
155 }
156
157 CBuilding::Update();
158 }
159
160 /*
161 void CFactory::SlowUpdate(void)
162 {
163 // this (ancient) code was intended to keep vicinity around factories clear
164 // so units could exit more easily among crowds of assisting builders, etc.
165 // it is unneeded now that units can flow / push through a non-moving crowd
166 // (so we no longer have to override CBuilding::SlowUpdate either)
167 if (transporter == NULL) {
168 CGameHelper::BuggerOff(pos - float3(0.01f, 0, 0.02f), radius, true, true, team, NULL);
169 }
170
171 CBuilding::SlowUpdate();
172 }
173 */
174
175
176
StartBuild(const UnitDef * buildeeDef)177 void CFactory::StartBuild(const UnitDef* buildeeDef) {
178 const float3& buildPos = CalcBuildPos();
179 const bool blocked = groundBlockingObjectMap->GroundBlocked(buildPos, this);
180
181 // wait until buildPos is no longer blocked (eg. by a previous buildee)
182 //
183 // it might rarely be the case that a unit got stuck inside the factory
184 // or died right after completion and left some wreckage, but that is up
185 // to players to fix (we no longer broadcast BuggerOff directives, since
186 // those are indiscriminate and ineffective)
187 if (blocked)
188 return;
189
190 UnitLoadParams buildeeParams = {buildeeDef, this, buildPos, ZeroVector, -1, team, buildFacing, true, false};
191 CUnit* buildee = unitLoader->LoadUnit(buildeeParams);
192
193 if (!unitDef->canBeAssisted) {
194 buildee->soloBuilder = this;
195 buildee->AddDeathDependence(this, DEPENDENCE_BUILDER);
196 }
197
198 AddDeathDependence(buildee, DEPENDENCE_BUILD);
199 script->StartBuilding();
200
201 // set curBuildDef to NULL to indicate construction
202 // has started, otherwise we would keep being called
203 curBuild = buildee;
204 curBuildDef = NULL;
205
206 if (losStatus[gu->myAllyTeam] & LOS_INLOS) {
207 Channels::General->PlayRandomSample(unitDef->sounds.build, buildPos);
208 }
209 }
210
UpdateBuild(CUnit * buildee)211 void CFactory::UpdateBuild(CUnit* buildee) {
212 if (IsStunned())
213 return;
214
215 // factory not under construction and
216 // nanolathing unit: continue building
217 lastBuildUpdateFrame = gs->frameNum;
218
219 // buildPiece is the rotating platform
220 const int buildPiece = script->QueryBuildInfo();
221 const float3& buildPos = CalcBuildPos(buildPiece);
222 const CMatrix44f& buildPieceMat = script->GetPieceMatrix(buildPiece);
223 const int buildPieceHeading = GetHeadingFromVector(buildPieceMat[2], buildPieceMat[10]); //! x.z, z.z
224
225 float3 buildeePos = buildPos;
226
227 // rotate unit nanoframe with platform
228 buildee->heading = (-buildPieceHeading + GetHeadingFromFacing(buildFacing)) & (SPRING_CIRCLE_DIVS - 1);
229
230 if (buildee->unitDef->floatOnWater && (buildeePos.y <= 0.0f))
231 buildeePos.y = -buildee->unitDef->waterline;
232
233 buildee->Move(buildeePos, false);
234 buildee->UpdateDirVectors(false);
235 buildee->UpdateMidAndAimPos();
236
237 const CCommandQueue& queue = commandAI->commandQue;
238
239 if (!queue.empty() && (queue.front().GetID() == CMD_WAIT)) {
240 buildee->AddBuildPower(this, 0.0f);
241 } else {
242 if (buildee->AddBuildPower(this, buildSpeed)) {
243 CreateNanoParticle();
244 }
245 }
246 }
247
FinishBuild(CUnit * buildee)248 void CFactory::FinishBuild(CUnit* buildee) {
249 if (buildee->beingBuilt) { return; }
250 if (unitDef->fullHealthFactory && buildee->health < buildee->maxHealth) { return; }
251
252 {
253 if (group && buildee->group == 0) {
254 buildee->SetGroup(group, true);
255 }
256 }
257
258 const CCommandAI* bcai = buildee->commandAI;
259 // if not idle, the buildee already has user orders
260 const bool buildeeIdle = (bcai->commandQue.empty());
261 const bool buildeeMobile = (dynamic_cast<const CMobileCAI*>(bcai) != NULL);
262
263 if (buildeeIdle || buildeeMobile) {
264 AssignBuildeeOrders(buildee);
265 waitCommandsAI.AddLocalUnit(buildee, this);
266 }
267
268 // inform our commandAI
269 finishedBuildFunc(this, finishedBuildCommand);
270 finishedBuildFunc = NULL;
271
272 eventHandler.UnitFromFactory(buildee, this, !buildeeIdle);
273 StopBuild();
274 }
275
276
277
QueueBuild(const UnitDef * buildeeDef,const Command & buildCmd,FinishBuildCallBackFunc buildFunc)278 unsigned int CFactory::QueueBuild(const UnitDef* buildeeDef, const Command& buildCmd, FinishBuildCallBackFunc buildFunc)
279 {
280 assert(!beingBuilt);
281 assert(buildeeDef != NULL);
282
283 if (finishedBuildFunc != NULL)
284 return FACTORY_KEEP_BUILD_ORDER;
285 if (curBuild != NULL)
286 return FACTORY_KEEP_BUILD_ORDER;
287 if (unitHandler->unitsByDefs[team][buildeeDef->id].size() >= buildeeDef->maxThisUnit)
288 return FACTORY_SKIP_BUILD_ORDER;
289 if (teamHandler->Team(team)->AtUnitLimit())
290 return FACTORY_KEEP_BUILD_ORDER;
291 if (!eventHandler.AllowUnitCreation(buildeeDef, this, NULL))
292 return FACTORY_SKIP_BUILD_ORDER;
293
294 finishedBuildFunc = buildFunc;
295 finishedBuildCommand = buildCmd;
296 curBuildDef = buildeeDef;
297 nextBuildUnitDefID = buildeeDef->id;
298
299 // signal that the build-order was accepted (queued)
300 return FACTORY_NEXT_BUILD_ORDER;
301 }
302
StopBuild()303 void CFactory::StopBuild()
304 {
305 // cancel a build-in-progress
306 script->StopBuilding();
307
308 if (curBuild) {
309 if (curBuild->beingBuilt) {
310 AddMetal(curBuild->metalCost * curBuild->buildProgress, false);
311 curBuild->KillUnit(NULL, false, true);
312 }
313 DeleteDeathDependence(curBuild, DEPENDENCE_BUILD);
314 }
315
316 curBuild = NULL;
317 curBuildDef = NULL;
318 finishedBuildFunc = NULL;
319 }
320
DependentDied(CObject * o)321 void CFactory::DependentDied(CObject* o)
322 {
323 if (o == curBuild) {
324 curBuild = NULL;
325 StopBuild();
326 }
327
328 CUnit::DependentDied(o);
329 }
330
331
332
SendToEmptySpot(CUnit * unit)333 void CFactory::SendToEmptySpot(CUnit* unit)
334 {
335 const float searchRadius = radius * 4.0f + unit->radius * 4.0f;
336 const int numSteps = 36;
337
338 const float3 exitPos = pos + frontdir*(radius + unit->radius);
339 float3 foundPos = pos + frontdir * searchRadius;
340 const float3 tempPos = foundPos;
341
342 for (int x = 0; x < numSteps; ++x) {
343 const float a = searchRadius * math::cos(x * PI / (numSteps * 0.5f));
344 const float b = searchRadius * math::sin(x * PI / (numSteps * 0.5f));
345
346 float3 testPos = pos + frontdir * a + rightdir * b;
347
348 if (!testPos.IsInBounds())
349 continue;
350 // don't pick spots behind the factory (because
351 // units will want to path through it when open
352 // which slows down production)
353 if ((testPos - pos).dot(frontdir) < 0.0f)
354 continue;
355
356 testPos.y = CGround::GetHeightAboveWater(testPos.x, testPos.z);
357
358 if (quadField->GetSolidsExact(testPos, unit->radius * 1.5f, 0xFFFFFFFF, CSolidObject::CSTATE_BIT_SOLIDOBJECTS).empty()) {
359 foundPos = testPos; break;
360 }
361 }
362
363 if (foundPos == tempPos) {
364 // no empty spot found, pick one randomly so units do not pile up even more
365 // also make sure not to loop forever if we happen to be facing a map border
366 foundPos.y = 0.0f;
367
368 do {
369 const float x = ((gs->randInt() * 1.0f) / RANDINT_MAX) * numSteps;
370 const float a = searchRadius * math::cos(x * PI / (numSteps * 0.5f));
371 const float b = searchRadius * math::sin(x * PI / (numSteps * 0.5f));
372
373 foundPos.x = pos.x + frontdir.x * a + rightdir.x * b;
374 foundPos.z = pos.z + frontdir.z * a + rightdir.z * b;
375 foundPos.y += 1.0f;
376 } while ((!foundPos.IsInBounds() || (foundPos - pos).dot(frontdir) < 0.0f) && (foundPos.y < 100.0f));
377
378 foundPos.y = CGround::GetHeightAboveWater(foundPos.x, foundPos.z);
379 }
380
381 // first queue a temporary waypoint outside the factory
382 // (otherwise units will try to turn before exiting when
383 // foundPos lies behind exit and cause jams / get stuck)
384 // we assume this temporary point is not itself blocked
385 //
386 // NOTE:
387 // MobileCAI::AutoGenerateTarget inserts a _third_
388 // command when |foundPos - tempPos| >= 100 elmos,
389 // because MobileCAI::FinishCommand only updates
390 // lastUserGoal for non-internal orders --> the
391 // final order given here should not be internal
392 // (and should also be more than CMD_CANCEL_DIST
393 // elmos distant from foundPos)
394 //
395 if (!unit->unitDef->canfly && exitPos.IsInBounds()) {
396 Command c0(CMD_MOVE, SHIFT_KEY, exitPos);
397 unit->commandAI->GiveCommand(c0);
398 }
399 Command c1(CMD_MOVE, SHIFT_KEY, foundPos);
400 unit->commandAI->GiveCommand(c1);
401 }
402
AssignBuildeeOrders(CUnit * unit)403 void CFactory::AssignBuildeeOrders(CUnit* unit) {
404 CCommandAI* unitCAI = unit->commandAI;
405 CCommandQueue& unitCmdQue = unitCAI->commandQue;
406
407 const CFactoryCAI* factoryCAI = static_cast<CFactoryCAI*>(commandAI);
408 const CCommandQueue& factoryCmdQue = factoryCAI->newUnitCommands;
409
410 if (factoryCmdQue.empty() && unitCmdQue.empty()) {
411 SendToEmptySpot(unit);
412 return;
413 }
414
415 Command c(CMD_MOVE);
416
417 if (!unit->unitDef->canfly) {
418 // HACK: when a factory has a rallypoint set far enough away
419 // to trigger the non-admissable path estimators, we want to
420 // avoid units getting stuck inside by issuing them an extra
421 // move-order. However, this order can *itself* cause the PF
422 // system to consider the path blocked if the extra waypoint
423 // falls within the factory's confines, so use a wide berth.
424 const float xs = unitDef->xsize * SQUARE_SIZE * 0.5f;
425 const float zs = unitDef->zsize * SQUARE_SIZE * 0.5f;
426
427 float tmpDst = 2.0f;
428 float3 tmpPos = unit->pos + (frontdir * this->radius * tmpDst);
429
430 if (buildFacing == FACING_NORTH || buildFacing == FACING_SOUTH) {
431 while ((tmpPos.z >= unit->pos.z - zs && tmpPos.z <= unit->pos.z + zs)) {
432 tmpDst += 0.5f;
433 tmpPos = unit->pos + (frontdir * this->radius * tmpDst);
434 }
435 } else {
436 while ((tmpPos.x >= unit->pos.x - xs && tmpPos.x <= unit->pos.x + xs)) {
437 tmpDst += 0.5f;
438 tmpPos = unit->pos + (frontdir * this->radius * tmpDst);
439 }
440 }
441
442 c.PushPos(tmpPos.cClampInBounds());
443 } else {
444 // dummy rallypoint for aircraft
445 c.PushPos(unit->pos);
446 }
447
448 if (unitCmdQue.empty()) {
449 unitCAI->GiveCommand(c);
450
451 // copy factory orders for new unit
452 for (CCommandQueue::const_iterator ci = factoryCmdQue.begin(); ci != factoryCmdQue.end(); ++ci) {
453 c = *ci;
454 c.options |= SHIFT_KEY;
455
456 if (c.GetID() == CMD_MOVE) {
457 const float3 p1 = c.GetPos(0);
458 const float3 p2 = float3(p1.x + gs->randFloat() * TWOPI, p1.y, p1.z + gs->randFloat() * TWOPI);
459 // apply a small amount of random jitter to move commands
460 // such that new units do not all share the same goal-pos
461 // and start forming a "trail" back to the factory exit
462 c.SetPos(0, p2);
463 }
464
465 unitCAI->GiveCommand(c);
466 }
467 } else {
468 unitCmdQue.push_front(c);
469 }
470 }
471
472
473
ChangeTeam(int newTeam,ChangeType type)474 bool CFactory::ChangeTeam(int newTeam, ChangeType type)
475 {
476 StopBuild();
477 return (CBuilding::ChangeTeam(newTeam, type));
478 }
479
480
CreateNanoParticle(bool highPriority)481 void CFactory::CreateNanoParticle(bool highPriority)
482 {
483 const int modelNanoPiece = nanoPieceCache.GetNanoPiece(script);
484
485 if (localModel == NULL || !localModel->HasPiece(modelNanoPiece))
486 return;
487
488 const float3 relNanoFirePos = localModel->GetRawPiecePos(modelNanoPiece);
489 const float3 nanoPos = this->GetObjectSpacePos(relNanoFirePos);
490
491 // unsynced
492 projectileHandler->AddNanoParticle(nanoPos, curBuild->midPos, unitDef, team, highPriority);
493 }
494