/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ #include "TransportUnit.h" #include "Game/GameHelper.h" #include "Game/SelectedUnitsHandler.h" #include "Map/Ground.h" #include "Sim/MoveTypes/MoveDefHandler.h" #include "Sim/MoveTypes/HoverAirMoveType.h" #include "Sim/MoveTypes/GroundMoveType.h" #include "Sim/Units/Scripts/CobInstance.h" #include "Sim/Units/CommandAI/CommandAI.h" #include "Sim/Units/BuildInfo.h" #include "Sim/Units/UnitDef.h" #include "Sim/Units/UnitLoader.h" #include "Sim/Units/UnitTypes/Building.h" #include "Sim/Misc/DamageArray.h" #include "Sim/Misc/LosHandler.h" #include "Sim/Misc/ModInfo.h" #include "Sim/Misc/TeamHandler.h" #include "Sim/Misc/QuadField.h" #include "System/EventHandler.h" #include "System/myMath.h" #include "System/creg/STL_List.h" CR_BIND_DERIVED(CTransportUnit, CUnit, ) CR_REG_METADATA(CTransportUnit, ( CR_MEMBER(transportedUnits), CR_MEMBER(transportCapacityUsed), CR_MEMBER(transportMassUsed), CR_RESERVED(16), CR_POSTLOAD(PostLoad) )) CR_BIND(CTransportUnit::TransportedUnit,) CR_REG_METADATA_SUB(CTransportUnit,TransportedUnit,( CR_MEMBER(unit), CR_MEMBER(piece), CR_MEMBER(size), CR_MEMBER(mass), CR_RESERVED(8) )) void CTransportUnit::Update() { CUnit::Update(); if (isDead) return; for (std::list::iterator ti = transportedUnits.begin(); ti != transportedUnits.end(); ++ti) { CUnit* transportee = ti->unit; transportee->mapSquare = mapSquare; float3 relPiecePos = ZeroVector; float3 absPiecePos = pos; if (ti->piece >= 0) { relPiecePos = script->GetPiecePos(ti->piece); absPiecePos = this->GetObjectSpacePos(relPiecePos); } if (unitDef->holdSteady) { // slave transportee orientation to piece if (ti->piece >= 0) { const CMatrix44f& transMat = GetTransformMatrix(true); const CMatrix44f& pieceMat = script->GetPieceMatrix(ti->piece); const CMatrix44f slaveMat = transMat * pieceMat; transportee->SetDirVectors(slaveMat); } } else { // slave transportee orientation to body transportee->heading = heading; transportee->updir = updir; transportee->frontdir = frontdir; transportee->rightdir = rightdir; } transportee->Move(absPiecePos, false); transportee->UpdateMidAndAimPos(); transportee->SetHeadingFromDirection(); // see ::AttachUnit if (transportee->IsStunned()) { quadField->MovedUnit(transportee); } } } void CTransportUnit::DependentDied(CObject* o) { // a unit died while we were carrying it std::list::iterator ti; for (ti = transportedUnits.begin(); ti != transportedUnits.end(); ++ti) { if (ti->unit != o) continue; transportCapacityUsed -= ti->size; transportMassUsed -= ti->mass; mass = Clamp(mass - ti->mass, CSolidObject::MINIMUM_MASS, CSolidObject::MAXIMUM_MASS); transportedUnits.erase(ti); break; } CUnit::DependentDied(o); } void CTransportUnit::KillUnit(CUnit* attacker, bool selfDestruct, bool reclaimed, bool) { if (!isDead) { // guard against recursive invocation via // transportee->KillUnit // helper->Explosion // helper->DoExplosionDamage // unit->DoDamage // unit->KillUnit // in the case that unit == this isDead = true; // ::KillUnit might be called multiple times while !deathScriptFinished, // but it makes no sense to kill/detach our transportees more than once std::list::iterator ti; for (ti = transportedUnits.begin(); ti != transportedUnits.end(); ++ti) { CUnit* transportee = ti->unit; assert(transportee != this); if (transportee->isDead) continue; transportee->SetTransporter(NULL); transportee->DeleteDeathDependence(this, DEPENDENCE_TRANSPORTER); transportee->UpdateVoidState(false); if (!unitDef->releaseHeld) { if (!selfDestruct) { // we don't want transportees to leave a corpse transportee->DoDamage(DamageArray(1e6f), ZeroVector, NULL, -DAMAGE_EXTSOURCE_KILLED, -1); } transportee->KillUnit(attacker, selfDestruct, reclaimed); } else { // NOTE: game's responsibility to deal with edge-cases now transportee->Move(transportee->pos.cClampInBounds(), false); // if this transporter uses the piece-underneath-ground // method to "hide" transportees, place transportee near // the transporter's place of death if (transportee->pos.y < CGround::GetHeightReal(transportee->pos.x, transportee->pos.z)) { const float r1 = transportee->radius + radius; const float r2 = r1 * std::max(unitDef->unloadSpread, 1.0f); // try to unload in a presently unoccupied spot // (if no such spot, unload on transporter wreck) for (int i = 0; i < 10; ++i) { float3 pos = transportee->pos; pos.x += (gs->randFloat() * 2.0f * r2 - r2); pos.z += (gs->randFloat() * 2.0f * r2 - r2); pos.y = CGround::GetHeightReal(pos.x, pos.z); if (!pos.IsInBounds()) continue; if (quadField->GetSolidsExact(pos, transportee->radius + 2.0f, 0xFFFFFFFF, CSolidObject::CSTATE_BIT_SOLIDOBJECTS).empty()) { transportee->Move(pos, false); break; } } } else { if (transportee->unitDef->IsGroundUnit()) { transportee->SetPhysicalStateBit(CSolidObject::PSTATE_BIT_FLYING); transportee->SetPhysicalStateBit(CSolidObject::PSTATE_BIT_SKIDDING); } } transportee->moveType->SlowUpdate(); transportee->moveType->LeaveTransport(); // issue a move order so that units dropped from flying // transports won't try to return to their pick-up spot if (unitDef->canfly && transportee->unitDef->canmove) { transportee->commandAI->GiveCommand(Command(CMD_MOVE, transportee->pos)); } transportee->SetStunned(transportee->paralyzeDamage > (modInfo.paralyzeOnMaxHealth? transportee->maxHealth: transportee->health)); transportee->SetVelocityAndSpeed(speed * (0.5f + 0.5f * gs->randFloat())); eventHandler.UnitUnloaded(transportee, this); } } transportedUnits.clear(); // make sure CUnit::KillUnit does not return early isDead = false; } CUnit::KillUnit(attacker, selfDestruct, reclaimed); } bool CTransportUnit::CanTransport(const CUnit* unit) const { if (unit->GetTransporter() != NULL) return false; if (!unit->unitDef->transportByEnemy && !teamHandler->AlliedTeams(unit->team, team)) return false; if (transportCapacityUsed >= unitDef->transportCapacity) return false; if (unit->unitDef->cantBeTransported) return false; // don't transport cloaked enemies if (unit->isCloaked && !teamHandler->AlliedTeams(unit->team, team)) return false; if (unit->xsize > (unitDef->transportSize * SPRING_FOOTPRINT_SCALE)) return false; if (unit->xsize < (unitDef->minTransportSize * SPRING_FOOTPRINT_SCALE)) return false; if (unit->mass >= CSolidObject::DEFAULT_MASS || unit->beingBuilt) return false; if (unit->mass < unitDef->minTransportMass) return false; if ((unit->mass + transportMassUsed) > unitDef->transportMass) return false; if (!CanLoadUnloadAtPos(unit->pos, unit)) return false; // check if is already (in)directly transporting const CTransportUnit* u = this; while (u != NULL) { if (u == unit) { return false; } u = u->GetTransporter(); } return true; } bool CTransportUnit::AttachUnit(CUnit* unit, int piece) { assert(unit != this); if (unit->GetTransporter() == this) { // assume we are already transporting this unit, // and just want to move it to a different piece // with script logic (this means the UnitLoaded // event is only sent once) std::list::iterator transporteesIt; for (transporteesIt = transportedUnits.begin(); transporteesIt != transportedUnits.end(); ++transporteesIt) { TransportedUnit& tu = *transporteesIt; if (tu.unit == unit) { tu.piece = piece; break; } } unit->UpdateVoidState(piece < 0); return false; } else { // handle transfers from another transport to us // (can still fail depending on CanTransport()) if (unit->GetTransporter() != NULL) { unit->GetTransporter()->DetachUnit(unit); } } // covers the case where unit->transporter != NULL if (!CanTransport(unit)) { return false; } AddDeathDependence(unit, DEPENDENCE_TRANSPORTEE); unit->AddDeathDependence(this, DEPENDENCE_TRANSPORTER); unit->SetTransporter(this); unit->loadingTransportId = -1; unit->SetStunned(!unitDef->isFirePlatform); unit->UpdateVoidState(piece < 0); if (unit->IsStunned()) { // make sure unit does not fire etc in transport selectedUnitsHandler.RemoveUnit(unit); } unit->UnBlock(); losHandler->FreeInstance(unit->los); radarHandler->RemoveUnit(unit); // do not remove unit from QF, otherwise projectiles // will not be able to connect with (ie. damage) it // // for NON-stunned transportees, QF position is kept // up-to-date by MoveType::SlowUpdate, otherwise by // ::Update // // quadField->RemoveUnit(unit); unit->los = NULL; if (dynamic_cast(unit) != NULL) { unitLoader->RestoreGround(unit); } if (dynamic_cast(moveType)) { unit->moveType->useHeading = false; } TransportedUnit tu; tu.unit = unit; tu.piece = piece; tu.size = unit->xsize / SPRING_FOOTPRINT_SCALE; tu.mass = unit->mass; transportCapacityUsed += tu.size; transportMassUsed += tu.mass; mass = Clamp(mass + tu.mass, CSolidObject::MINIMUM_MASS, CSolidObject::MAXIMUM_MASS); transportedUnits.push_back(tu); unit->moveType->StopMoving(true, true); unit->CalculateTerrainType(); unit->UpdateTerrainType(); eventHandler.UnitLoaded(unit, this); commandAI->BuggerOff(pos, -1.0f); return true; } bool CTransportUnit::DetachUnitCore(CUnit* unit) { if (unit->GetTransporter() != this) return false; std::list::iterator ti; for (ti = transportedUnits.begin(); ti != transportedUnits.end(); ++ti) { if (ti->unit != unit) continue; this->DeleteDeathDependence(unit, DEPENDENCE_TRANSPORTEE); unit->DeleteDeathDependence(this, DEPENDENCE_TRANSPORTER); unit->SetTransporter(NULL); if (dynamic_cast(moveType)) { unit->moveType->useHeading = true; } // de-stun detaching units in case we are not a fire-platform unit->SetStunned(unit->paralyzeDamage > (modInfo.paralyzeOnMaxHealth? unit->maxHealth: unit->health)); unit->moveType->SlowUpdate(); unit->moveType->LeaveTransport(); if (CBuilding* building = dynamic_cast(unit)) building->ForcedMove(building->pos); transportCapacityUsed -= ti->size; transportMassUsed -= ti->mass; mass = Clamp(mass - ti->mass, CSolidObject::MINIMUM_MASS, CSolidObject::MAXIMUM_MASS); transportedUnits.erase(ti); unit->UpdateVoidState(false); unit->CalculateTerrainType(); unit->UpdateTerrainType(); eventHandler.UnitUnloaded(unit, this); return true; } return false; } bool CTransportUnit::DetachUnit(CUnit* unit) { if (DetachUnitCore(unit)) { unit->Block(); // erase command queue unless it's a wait command const CCommandQueue& queue = unit->commandAI->commandQue; if (queue.empty() || (queue.front().GetID() != CMD_WAIT)) { unit->commandAI->GiveCommand(Command(CMD_STOP)); } return true; } return false; } bool CTransportUnit::DetachUnitFromAir(CUnit* unit, const float3& pos) { if (DetachUnitCore(unit)) { unit->Drop(this->pos, this->frontdir, this); // add an additional move command for after we land if (unit->unitDef->canmove) { unit->commandAI->GiveCommand(Command(CMD_MOVE, pos)); } return true; } return false; } bool CTransportUnit::CanLoadUnloadAtPos(const float3& wantedPos, const CUnit* unit, float* wantedHeightPtr) const { bool canLoadUnload = false; float wantedHeight = GetTransporteeWantedHeight(wantedPos, unit, &canLoadUnload); if (wantedHeightPtr != NULL) *wantedHeightPtr = wantedHeight; return canLoadUnload; } float CTransportUnit::GetTransporteeWantedHeight(const float3& wantedPos, const CUnit* unit, bool* allowedPos) const { bool isAllowedHeight = true; float wantedHeight = unit->pos.y; float clampedHeight = wantedHeight; const UnitDef* transporteeUnitDef = unit->unitDef; const MoveDef* transporteeMoveDef = unit->moveDef; if (unit->GetTransporter() != NULL) { // if unit is being transported, set // to the altitude at which to UNload the transportee wantedHeight = CGround::GetHeightReal(wantedPos.x, wantedPos.z); isAllowedHeight = transporteeUnitDef->CheckTerrainConstraints(transporteeMoveDef, wantedHeight, &clampedHeight); if (isAllowedHeight) { if (transporteeMoveDef != NULL) { // transportee is a mobile ground unit switch (transporteeMoveDef->speedModClass) { case MoveDef::Ship: { wantedHeight = std::max(-transporteeUnitDef->waterline, wantedHeight); clampedHeight = wantedHeight; } break; case MoveDef::Hover: { wantedHeight = std::max(0.0f, wantedHeight); clampedHeight = wantedHeight; } break; default: { } break; } } else { // transportee is a building or an airplane wantedHeight *= (1 - transporteeUnitDef->floatOnWater); clampedHeight = wantedHeight; } } if (dynamic_cast(unit) != NULL) { // for transported structures, must be free/buildable // (note: TestUnitBuildSquare calls CheckTerrainConstraints again) BuildInfo bi(transporteeUnitDef, wantedPos, unit->buildFacing); bi.pos = CGameHelper::Pos2BuildPos(bi, true); CFeature* f = NULL; if (isAllowedHeight && (!CGameHelper::TestUnitBuildSquare(bi, f, -1, true) || f != NULL)) isAllowedHeight = false; } } float rawContactHeight = clampedHeight + unit->height; float modContactHeight = rawContactHeight; // *we* must be capable of reaching the point-of-contact height // however this check fails for eg. ships that want to (un)load // land units on shore --> would require too many special cases // therefore restrict its use to transport aircraft if (this->moveDef == NULL) { isAllowedHeight &= unitDef->CheckTerrainConstraints(NULL, rawContactHeight, &modContactHeight); } if (allowedPos != NULL) { *allowedPos = isAllowedHeight; } return modContactHeight; } short CTransportUnit::GetTransporteeWantedHeading(const CUnit* unit) const { if (unit->GetTransporter() == NULL) return unit->heading; if (dynamic_cast(moveType) == NULL) return unit->heading; if (dynamic_cast(unit) == NULL) return unit->heading; // transported structures want to face a cardinal direction return (GetHeadingFromFacing(unit->buildFacing)); }