/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see .
*/
////////////////////////////////////////////////////////////////////////////////
// //
// (c) 2001-2003 Electronic Arts Inc. //
// //
////////////////////////////////////////////////////////////////////////////////
// FILE: RailroadBehavior.cpp //////////////////////////////////////////////////////////////
// Author: Mark Lorenzen, Sept 2002
// Desc: The railroad track following railroad carriage/locomotive logic module
///////////////////////////////////////////////////////////////////////////////////////////////////
#include "PreRTS.h"
#include "Common/Player.h"
#include "Common/ThingFactory.h"
#include "Common/ThingTemplate.h"
#include "GameLogic/Locomotor.h"
#include "GameLogic/Module/RailroadGuideAIUpdate.h"
#include "GameLogic/Object.h"
#include "GameLogic/PartitionManager.h"
#include "GameLogic/Module/ContainModule.h"
#include "GameLogic/Module/DemoTrapUpdate.h"
#include "GameLogic/AI.h"
#include "GameLogic/AIPathfind.h"
#include "GameClient/Drawable.h"
#include "GameClient/Statistics.h"
///#define RAILROAD_DESYNC_TEST
#ifdef RAILROAD_DESYNC_TEST
#include "winbase.h"
#endif
// TYPES //////////////////////////////////////////////////////////////////////////////////////////
static const Int INVALID_PATH = -1;
#define NORMAL_VEL_Z 0.25f
#define NORMAL_MASS 50.0f
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
#define FRAMES_UNPULLED_LONG_ENOUGH_TO_UNHITCH (2)
void TrainTrack::incReference()
{
++m_refCount;
}
Bool TrainTrack::releaseReference()
{
return ( --m_refCount == 0 );
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
RailroadBehaviorModuleData::RailroadBehaviorModuleData( void )
{
m_carriageTemplateNameData.clear();
m_pathPrefixName.clear();
m_CrashFXTemplateName.clear();
m_isLocomotive = FALSE;
m_runningGarrisonSpeedMax = 1.0f;
m_killSpeedMin = 1.0f;
m_speedMax = 4;
m_acceleration = 1.01f;
m_braking = 0.99f;
m_friction = 0.97f;
m_waitAtStationTime = 150;
} // end RailroadBehaviorModuleData
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
RailroadBehavior::RailroadBehavior( Thing *thing, const ModuleData *moduleData )
: PhysicsBehavior( thing, moduleData )
{
const RailroadBehaviorModuleData *modData = getRailroadBehaviorModuleData();
m_carriageTemplateNameIterator = 0;
m_nextStationTask = DO_NOTHING;
m_trailerID = INVALID_ID;
conductorPullInfo.speed = 0;
conductorPullInfo.m_direction = 1.0f;
conductorPullInfo.m_mostRecentSpecialPointHandle = 0xfacade;
conductorPullInfo.towHitchPosition.set(0,0,0);
conductorPullInfo.trackDistance = 0;
conductorPullInfo.currentWaypoint = 0xfacade;
conductorPullInfo.previousWaypoint = 0xfacade;
m_pullInfo.speed = 0.0f;
m_pullInfo.m_direction = 1.0f;
m_pullInfo.m_mostRecentSpecialPointHandle = 0xfacade;
m_pullInfo.towHitchPosition.set(0,0,0);
m_pullInfo.trackDistance = 0.0f;
m_pullInfo.currentWaypoint = 0xfacade;
m_pullInfo.previousWaypoint = 0xfacade;
#ifdef RAILROAD_DESYNC_TEST
_LARGE_INTEGER pc;
QueryPerformanceCounter( &pc ); // absolutely, positively random every call!
Real random = 100000.0f / (Real)pc.LowPart;
conductorPullInfo.m_direction = random;
m_pullInfo.m_direction = random;
#endif
m_runningSound = modData->m_runningSound;
m_clicketyClackSound = modData->m_clicketyClackSound;
m_whistleSound = modData->m_whistleSound;
m_runningSound.setObjectID( getObject()->getID() );
m_whistleSound.setObjectID( getObject()->getID() ) ;
m_clicketyClackSound.setObjectID( getObject()->getID() ) ;
m_track = NULL;
m_currentPointHandle = 0xfacade;
m_waitAtStationTimer = 0;
m_anchorWaypointID = INVALID_WAYPOINT_ID;
m_carriagesCreated = FALSE;
m_hasEverBeenHitched = FALSE;
m_trackDataLoaded = FALSE;
m_waitingInWings = TRUE;
m_endOfLine = FALSE;
m_isLocomotive = modData->m_isLocomotive;
m_isLeadCarraige = m_isLocomotive; // for now, I am the lead, only if I am the locomotive
m_wantsToBeLeadCarraige = FALSE;
m_disembark = FALSE;
m_inTunnel = FALSE;
m_held = FALSE;
m_conductorState = m_isLocomotive ? ACCELERATE : COAST;
} // end RailroadBehavior
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
RailroadBehavior::~RailroadBehavior( void )
{
TheAudio->removeAudioEvent( m_runningSound.getPlayingHandle() );// no more chugchug when I'm dead
if( m_track != NULL )
{
if (m_track->releaseReference())
delete m_track;
m_track = NULL;
}
} // end ~RailroadBehavior
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
Bool RailroadBehavior::isRailroad() const
{
if ( ! m_track )
return FALSE;// I haven't even started yet!
if (m_waitingInWings)
return FALSE;
if (m_endOfLine)
return FALSE;
if (m_isLeadCarraige)
return TRUE;
if (m_trailerID==INVALID_ID)
return TRUE;
//Explanation: I return that I am a railroad only if I am at one of the ends, otherwise I refuse to cooperate
return FALSE;
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void RailroadBehavior::onCollide( Object *other, const Coord3D *loc, const Coord3D *normal )
{
Object *obj = getObject();
if ( ! obj)
return; //sanity
if ( ! other)
return; //sanity
if ( ! loc)
return; //sanity
if ( ! normal)
return; //sanity
if (m_waitingInWings)
return ;
if (m_endOfLine)
return ;
for (BehaviorModule** m = other->getBehaviorModules(); *m; ++m)
{
CollideModuleInterface* collide = (*m)->getCollide();
if (!collide)
continue;
if( collide->isRailroad())
{
if ( m_isLocomotive )//yikes! I just rear ended a stranded train! Oh well...
{
other->kill();
}
else if ( m_isLeadCarraige ) //yikes! I just coasted into some other carriage
{
other->kill();
obj->kill();//and I am dead, too
}
return ;//// its a train, folks
}
}
if (other->isKindOf( KINDOF_STRUCTURE ) )// now be careful here, we ignore buildings, except for hostile, nasty ones that can blow us up
{
//If it is a civilian building like a tunnel or a train station, let it be
//but if it is a faction building, kill it!
if (other->isKindOf( KINDOF_FS_POWER ) ||
other->isKindOf( KINDOF_FS_FACTORY ) ||
other->isKindOf( KINDOF_FS_BASE_DEFENSE ) ||
other->isKindOf( KINDOF_FS_TECHNOLOGY ) ||
other->isKindOf( KINDOF_REBUILD_HOLE ) )
{
playImpactSound(other, other->getPosition());
other->kill();
return;
}
static NameKeyType key_DemoTrapUpdate = NAMEKEY("DemoTrapUpdate");
DemoTrapUpdate *dtu = (DemoTrapUpdate*)other->findUpdateModule(key_DemoTrapUpdate);
if( dtu )
{
if( !other->getStatusBits().test( OBJECT_STATUS_UNDER_CONSTRUCTION ) )
obj->kill(); // it can only detonate on me if it is ready
playImpactSound(other, other->getPosition());
other->kill();
return;
}
}
PhysicsBehavior *theirPhys = other->getPhysics();
if( ! theirPhys )
return;
// maybe we don't want to trample this unit?
const RailroadBehaviorModuleData *modData = getRailroadBehaviorModuleData();
if ( m_conductorState == WAIT_AT_STATION && (m_pullInfo.speed < modData->m_runningGarrisonSpeedMax) ) // they can grab on safely
{
AIUpdateInterface *ai = other->getAI();
if (ai && ai->getEnterTarget() == obj) // other intends to garrison me.
{
return;
}
}
if (other->getContainedBy() == getObject() )//I dont mash my own sweet passengers
return;
//if ( other->getModelConditionFlags().test( MODELCONDITION_RUBBLE ) == TRUE )
// return; // I don't mess with rubble
Bool victimIsInfantry = other->isKindOf( KINDOF_INFANTRY );
const Coord3D *myDir = obj->getUnitDirectionVector2D();
const Coord3D *myLoc = obj->getPosition();
const Coord3D *theirLoc = other->getPosition();
//---------------------------
// if we made it this far, it is something we dont want to share space with
Coord3D dlt;
dlt.x = theirLoc->x - myLoc->x;
dlt.y = theirLoc->y - myLoc->y;
dlt.z = theirLoc->z - myLoc->z;
//Alert all the players of recent disaster
if ( ! m_whistleSound.isCurrentlyPlaying())
m_whistleSound.setPlayingHandle(TheAudio->addAudioEvent( &m_whistleSound ));
Real dist = (Real)sqrtf( dlt.x*dlt.x + dlt.y*dlt.y + dlt.z*dlt.z);
Real usRadius = obj->getGeometryInfo().getMajorRadius();
Real themRadius = other->getGeometryInfo().getMajorRadius();
Real overlap = ((usRadius + themRadius) - dist) + 1;// the plus 1 makes them go just outside of me.
dlt.normalize();
if ( ! victimIsInfantry )
{
overlap/= 4;
dlt.scale( overlap);
Coord3D newPos;
newPos.x = theirLoc->x + dlt.x;
newPos.y = theirLoc->y + dlt.y;
newPos.z = theirLoc->z + dlt.z;
other->setPosition( &newPos );
}
if ( m_conductorState == WAIT_AT_STATION || (m_conductorState == COAST && m_pullInfo.speed < modData->m_runningGarrisonSpeedMax) || !m_isLocomotive )
{
// AIUpdateInterface *ai = other->getAI();
// if ( ai )
// ai->aiIdle( CMD_FROM_AI );// this eliminates yadda by telling them to stop driving into me
return;//let those trying to board pass through unhindered
}
//figure out the relative slope between them and me
Coord3D delta = *theirLoc;
delta.sub( myLoc );
delta.normalize();
Real dot = delta.x * myDir->x + delta.y * myDir->y + delta.z * myDir->z;
if (other->isEffectivelyDead())
{// we just run over debris, instead of shoving it around
delta.scale( MIN(0.3f,m_pullInfo.speed * 0.66f) );
}
else
{
delta.scale( MIN(1.4f,m_pullInfo.speed * 0.66f) );// the faster I go, the harder I slam!
//Absolute death to be hit by a train, no survival
if ( m_pullInfo.speed >= modData->m_killSpeedMin ) // they can grab on safely
{
other->kill();
theirPhys->setPitchRate(GameLogicRandomValueReal(-0.03f, 0.03f));
theirPhys->setRollRate(GameLogicRandomValueReal(-0.03f, 0.03f));
}
else
{
playImpactSound(other, loc); //Play a bounce sound
DamageInfo damageInfo;
damageInfo.in.m_damageType = DAMAGE_CRUSH;
damageInfo.in.m_deathType = DEATH_CRUSHED;
damageInfo.in.m_sourceID = getObject()->getID();
damageInfo.in.m_amount = m_pullInfo.speed * 10; // hurt!
other->attemptDamage( &damageInfo );
}
}
Coord3D heft = *theirLoc;
heft.z = MAX(heft.z, TheTerrainLogic->getGroundHeight(heft.x, heft.y) + 2); // lift them off the ground
other->setPosition(&heft);
delta.z = GameLogicRandomValueReal(0.05f, m_pullInfo.speed/10); // for some fake heft
delta.scale(dot);
// This is a special check so that it wont hurl infantry clear across the map!
if( ! ( victimIsInfantry && theirPhys->getVelocityMagnitude() > 5.0f ) )
theirPhys->addVelocityTo( &delta );
theirPhys->setAllowToFall( TRUE );
theirPhys->setAllowBouncing( TRUE );
theirPhys->setAllowAirborneFriction( TRUE );
const Coord3D up = {0,0,1};
Coord3D cross;
myDir->crossProduct( myDir, &up, &cross );
delta.normalize();
Real deviationCOG = cross.x * delta.x + cross.y * delta.y + cross.z * delta.z;
if (dot > 0)
theirPhys->setYawRate( deviationCOG * -0.06f * m_pullInfo.speed);
} // end RailroadBehavior:: on collide
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::playImpactSound(Object *victim, const Coord3D *impactPosition)
{
AudioEventRTS impact;
//AudioEventRTS *pImpact = &impact;
PhysicsBehavior *theirPhys = victim->getPhysics();
Bool hasBounceSound = FALSE;
if ( theirPhys )
{
const AudioEventRTS* bsound = theirPhys->getBounceSound();
if (bsound)
{
impact = *bsound;//copy theirs
hasBounceSound = TRUE;
}
}
if ( ! hasBounceSound )
{
const RailroadBehaviorModuleData *modData = getRailroadBehaviorModuleData();
if (victim->isKindOf( KINDOF_INFANTRY ))
{
impact = modData->m_meatyImpactDefaultSound;//copy
}
else if (victim->isKindOf( KINDOF_HUGE_VEHICLE ) || victim->isKindOf( KINDOF_STRUCTURE ))
{
impact = modData->m_bigMetalImpactDefaultSound;//copy
}
else if (victim->isKindOf( KINDOF_VEHICLE ) )
{
impact = modData->m_smallMetalImpactDefaultSound;//copy
}
}
if (impact.getEventName().isEmpty())
return;
Real vel = NORMAL_VEL_Z; //the max
Real mass = NORMAL_MASS; //the max
impact.setPosition(impactPosition);
if ( theirPhys )
{
vel += fabs(theirPhys->getVelocity()->length());
mass += fabs(theirPhys->getMass());
vel /= 2;
mass /= 2;//average of him and me
impact.setPosition(victim->getPosition());
}
vel = MIN(NORMAL_VEL_Z, MAX(0, vel));
mass = MIN(NORMAL_MASS, MAX(0, mass));
Real volAdjust = NormalizeToRange(MuLaw(vel, NORMAL_VEL_Z, 500), -1, 1, 0.25, 1.0);
volAdjust *= NormalizeToRange(MuLaw(mass, NORMAL_MASS, 500), -1, 1, 0.25, 1.0);
impact.setVolume( volAdjust );
// This sound really should only be played at the position where the collision occurs.
// Therefore, set the player index of the sound to be the player index of the victim object.
impact.setPlayerIndex( victim->getControllingPlayer()->getPlayerIndex() );
TheAudio->addAudioEvent( &impact );
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::loadTrackData( void )
{
if ( m_track != NULL )
return;// lets do this only once!
//First we scan the map for the nearest waypoint
Object *obj = getObject();
const Coord3D *myPos = obj->getPosition();
Coord3D delta;
Real closestDistance = 99999.9f;
// m_anchorWaypointID COULD HAVE BEEN RECORDED IN XFER, WHICH MEANS I LOADED MY TRACK DATA IN A PRIOR LIFE,
// SO LETS JUST RE_INIT THE TRACK BASED ON THAT POINT, AUTOMAGICALLY
Waypoint *anchorWaypoint = NULL;
if ( m_anchorWaypointID == INVALID_WAYPOINT_ID )
{
Waypoint *anyWaypoint = TheTerrainLogic->getFirstWaypoint();
while ( anyWaypoint )
{
//measure the distance from me to the waypoint
delta.x = myPos->x - anyWaypoint->getLocation()->x;
delta.y = myPos->y - anyWaypoint->getLocation()->y;
delta.z = myPos->z - anyWaypoint->getLocation()->z;
if (closestDistance > delta.length() )
{
closestDistance = delta.length();
anchorWaypoint = anyWaypoint;
m_anchorWaypointID = anchorWaypoint->getID();// save for XFER, later
}
anyWaypoint = anyWaypoint->getNext();
}
}
else
{
anchorWaypoint = TheTerrainLogic->getWaypointByID( m_anchorWaypointID );
}
DEBUG_ASSERTCRASH( anchorWaypoint, ( "Railroad... couldn't find a waypoint close enough to be the anchor.\n You should put your train engine right on a waypoint,\n and make sure the path forms a valid track.") );
if ( ! anchorWaypoint )
return;
m_track = NEW( TrainTrack );// this constructor inc's the refcount to 1
// From now until the next carriage is added, this track is writable using getWritablePointList();
// This method will return NULL when refcount is 2 or more
// getPointList returns the list as const to any caller
// each carriage must increment the reference when this pointer is passed to it,
// any subsequent carriage (destructor) will delete this memory here if it releases refcount to zero
Coord3D fromPos, toPos, fromToDelta;
m_track->m_length = 0.0f;
Waypoint *scanner = anchorWaypoint;
Real distFromTo = 0.0f;
//Let's start buliding our own track data from the waypoint data we find
TrackPointList* track = m_track->getWritablePointList();
TrackPoint trackPoint; // local workspace
if ( scanner && track)
{
trackPoint.m_distanceFromPrev = 0;
trackPoint.m_distanceFromFirst = 0;
trackPoint.m_isFirstPoint = TRUE;
trackPoint.m_isLastPoint = FALSE;
trackPoint.m_isTunnelOrBridge = scanner->getName().endsWith("Tunnel");
trackPoint.m_isStation = scanner->getName().endsWith("Station");
trackPoint.m_isDisembark = scanner->getName().endsWith("Disembark");
trackPoint.m_isPingPong = FALSE;
trackPoint.m_position.set( scanner->getLocation() );
trackPoint.m_handle = scanner->getID();
track->push_back( trackPoint );
}
while ( scanner )
{
trackPoint.clear();// clean up the local workspace to make debugging more fun
if ( scanner->getNumLinks() )
{
Waypoint *anotherWaypoint = scanner->getLink( 0 );
// if scanner's link is valid, we'll add it to the track. now
if ( anotherWaypoint != NULL )
{
//measure the track while we are at it
fromPos = *scanner->getLocation();
toPos = *anotherWaypoint->getLocation();
fromToDelta.x = fromPos.x - toPos.x;
fromToDelta.y = fromPos.y - toPos.y;
fromToDelta.z = fromPos.z - toPos.z;
distFromTo = fromToDelta.length();
m_track->m_length += distFromTo;
trackPoint.m_distanceFromPrev = distFromTo;
trackPoint.m_distanceFromFirst = m_track->m_length;
trackPoint.m_isFirstPoint = FALSE;
trackPoint.m_isLastPoint = anotherWaypoint->getLink( 0 ) == NULL;
trackPoint.m_isTunnelOrBridge = anotherWaypoint->getName().endsWith("Tunnel");
trackPoint.m_isStation = anotherWaypoint->getName().endsWith("Station");
trackPoint.m_isPingPong = scanner->getName().endsWith("PingPong");
trackPoint.m_isDisembark = scanner->getName().endsWith("Disembark");
trackPoint.m_position.set( anotherWaypoint->getLocation() );
trackPoint.m_handle = scanner->getID();
track->push_back( trackPoint );
}
scanner = anotherWaypoint;
}
else
break; // since we have reached a non-linked waypoint
if ( scanner == anchorWaypoint )
{
m_track->m_isLooping = TRUE;
break; // it must be a looping track. Cool.
}
}
} // end loadTrackData
void RailroadBehavior::makeAWallOutOfThisTrain( Bool on )
{
if ( on == TRUE )
TheAI->pathfinder()->createAWallFromMyFootprint( getObject() ); // Temporarily treat this object as an obstacle.
else
TheAI->pathfinder()->removeWallFromMyFootprint( getObject() ); // Undo createAWallFromMyFootprint.
if ( m_trailerID != INVALID_ID )
{
Object *trailer = TheGameLogic->findObjectByID( m_trailerID );
if ( trailer )
{
static NameKeyType key_RGUpdate = NAMEKEY("RailroadBehavior");
RailroadBehavior *RGUpdate = (RailroadBehavior*)trailer->findUpdateModule(key_RGUpdate);
if( RGUpdate )
{
RGUpdate->makeAWallOutOfThisTrain( on ); // recursive down the train
}
}
}
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
UpdateSleepTime RailroadBehavior::update( void )
{
// load the waypoint data if not loaded
if( m_trackDataLoaded == FALSE && m_isLocomotive )
{
loadTrackData();
if ( m_track )
createCarriages();
m_trackDataLoaded = TRUE;
}
if ( ! m_track )
{
return UPDATE_SLEEP_NONE;
}
//Object *us = getObject();
const RailroadBehaviorModuleData *modData = getRailroadBehaviorModuleData();
if ( m_isLocomotive )
{
if ( m_conductorState == APPLY_BRAKES )
{
conductorPullInfo.speed *= modData->m_braking;
if (fabs(conductorPullInfo.speed) < 0.1f)
{
conductorPullInfo.speed = 0;
///////////////////////////////////////( &m_hissySteamSound );
m_waitAtStationTimer = modData->m_waitAtStationTime;
m_conductorState = WAIT_AT_STATION;
makeAWallOutOfThisTrain( TRUE );
if ( m_disembark )
{
disembark();
m_disembark = FALSE;
}
}
}
else if ( m_conductorState == WAIT_AT_STATION)
{
--m_waitAtStationTimer;
if ( m_waitAtStationTimer <= 0 && (!m_held) )
{
m_conductorState = ACCELERATE;
conductorPullInfo.speed = 0.05f * conductorPullInfo.m_direction;
m_runningSound.setPlayingHandle(TheAudio->addAudioEvent( &m_runningSound ));
makeAWallOutOfThisTrain( FALSE );
}
else if ( m_waitAtStationTimer == (modData->m_waitAtStationTime/4) )
{
m_whistleSound.setPlayingHandle(TheAudio->addAudioEvent( &m_whistleSound ));
}
}
else if ( m_conductorState == ACCELERATE )
{
conductorPullInfo.speed += 0.02f * conductorPullInfo.m_direction; // push start multiplier
conductorPullInfo.speed *= modData->m_acceleration;
if ( conductorPullInfo.speed > modData->m_speedMax)
{
conductorPullInfo.speed = modData->m_speedMax;
}
else if ( conductorPullInfo.speed < -modData->m_speedMax)
{
conductorPullInfo.speed = -modData->m_speedMax;
}
if ( ! m_runningSound.isCurrentlyPlaying() )
m_runningSound.setPlayingHandle(TheAudio->addAudioEvent( &m_runningSound ));
}
}
if ( m_wantsToBeLeadCarraige > FRAMES_UNPULLED_LONG_ENOUGH_TO_UNHITCH )//if this flag survived until now, I have lost my puller
{
m_isLeadCarraige = TRUE;
}
if ( m_isLeadCarraige )
{
if ( m_conductorState == COAST )
{
conductorPullInfo.speed *= modData->m_friction;
TheAudio->removeAudioEvent( m_runningSound.getPlayingHandle() );
}
conductorPullInfo.trackDistance += conductorPullInfo.speed ;
// only normalize track position for a looping track, otherwise, let train exit by exceeding tracklength
while ( (conductorPullInfo.trackDistance > m_track->m_length) && m_track->m_isLooping)
conductorPullInfo.trackDistance -= m_track->m_length;
while ( (conductorPullInfo.trackDistance < 0.0f ) && m_track->m_isLooping)
conductorPullInfo.trackDistance += m_track->m_length;
FindPosByPathDistance( &conductorPullInfo.towHitchPosition,
conductorPullInfo.trackDistance,
m_track->m_length);
//let the conductor pull "me" while reseting my info, then...
updatePositionTrackDistance( &conductorPullInfo, &m_pullInfo);
//get pointer to my trailer
Object *trailer = TheGameLogic->findObjectByID( m_trailerID );
if ( trailer )
{
//call his pull trailer with my info;
static NameKeyType key_RGUpdate = NAMEKEY("RailroadBehavior");
RailroadBehavior *RGUpdate = (RailroadBehavior*)trailer->findUpdateModule(key_RGUpdate);
if( RGUpdate )
{
RGUpdate->getPulled( &m_pullInfo );
}
}
else
{
m_trailerID = INVALID_ID;// so I will forget about my trailer and designate myself as the caboose
if ( m_endOfLine )
TheGameLogic->destroyObject( getObject() );
}
}
else if ( m_wantsToBeLeadCarraige <= FRAMES_UNPULLED_LONG_ENOUGH_TO_UNHITCH )// if I am not the lead carriage
{
m_wantsToBeLeadCarraige ++; // like every young carriage, I aspire to be the lead carriage some day
// unless getpulled() set this false, I will be on the next update! Joy!
}
Drawable *draw = getObject()->getDrawable();
if (draw)
{
if ( ! m_track->m_isLooping )
{
draw->setDrawableHidden( m_waitingInWings || m_endOfLine );
}
// TURN OFF SMOKE EFFECTS IF WE ARE IN A TUNNEL
const Coord3D *drawPos = draw->getPosition();
if (drawPos->z < TheTerrainLogic->getGroundHeight(drawPos->x, drawPos->y) - 3.0f )
draw->setModelConditionState(MODELCONDITION_OVER_WATER);
else
draw->clearModelConditionState(MODELCONDITION_OVER_WATER);
}
return UPDATE_SLEEP_NONE;
} // end update
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void RailroadBehavior::disembark(void)
{
ContainModuleInterface *contain = getObject()->getContain();
if (contain)
{
contain->orderAllPassengersToExit( CMD_FROM_AI, FALSE );
}
Object *trailer = TheGameLogic->findObjectByID( m_trailerID );
if ( trailer )
{
static NameKeyType key_RGUpdate = NAMEKEY("RailroadBehavior");
RailroadBehavior *RGUpdate = (RailroadBehavior*)trailer->findUpdateModule(key_RGUpdate);
if( RGUpdate )
{
RGUpdate->disembark();
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE ////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
class PartitionFilterIsValidCarriage : public PartitionFilter
{
private:
Object* m_obj;
const RailroadBehaviorModuleData* m_data;
public:
PartitionFilterIsValidCarriage(Object* obj, const RailroadBehaviorModuleData* data) : m_obj(obj), m_data(data) { }
#if defined(_DEBUG) || defined(_INTERNAL)
virtual const char* debugGetName() { return "PartitionFilterIsValidCarriage"; }
#endif
virtual Bool allow(Object *objOther)
{
// must exist!
if ( m_obj == NULL || objOther == NULL)
return FALSE;
//must not be me!
if (m_obj == objOther)
return FALSE;
// must have railroady goodness
static NameKeyType key_rb = NAMEKEY("RailroadBehavior");
RailroadBehavior *rb = (RailroadBehavior*)objOther->findUpdateModule(key_rb);
if( ! rb )
return FALSE;
if ( rb->hasEverBeenHitched() )// two of us can't pull you
return FALSE;
// must be our ally (well, maybe)
if (m_obj->getRelationship(objOther) != ALLIES)
return FALSE;
// guess it's carriage-worthy!
return TRUE;
}
};
void RailroadBehavior::createCarriages( void )
{
if ( ! m_isLocomotive )
{
DEBUG_ASSERTCRASH(m_isLocomotive, ("a not locomotive attempted to create carriages"));
return;
}
const RailroadBehaviorModuleData* md = getRailroadBehaviorModuleData();
Object *self = getObject();
//First we'll see if the map artist put some cars down on the track for us to find
Real maxRadius = self->getGeometryInfo().getMajorRadius() * 2.0f;
Coord3D myHitchLoc = *self->getPosition();
Coord3D hitchOffset = *self->getUnitDirectionVector2D();//copy that
hitchOffset.scale ( - maxRadius );// negative, since I want the back, not the front
myHitchLoc.add( & hitchOffset );
PartitionFilterIsValidCarriage pfivc(self, md);
PartitionFilter *filters[] = { &pfivc, 0 };
Object* xferCarriage = NULL;
Object *closeCarriage = NULL;
Object *firstCarriage = NULL;
if ( m_trailerID != INVALID_ID )
{
xferCarriage = TheGameLogic->findObjectByID( m_trailerID );
}
if (xferCarriage != NULL)
closeCarriage = xferCarriage;
else
closeCarriage = ThePartitionManager->getClosestObject( &myHitchLoc, maxRadius, FROM_CENTER_2D, filters);
TemplateNameList list = md->m_carriageTemplateNameData;
TemplateNameIterator iter = list.begin();
if ( iter != list.end() )
{
const ThingTemplate* temp = TheThingFactory->findTemplate( *iter );
if (temp)
{
if ( closeCarriage )
firstCarriage = closeCarriage;
else // or else let's use the defualt template list prvided in the INI
{
firstCarriage = TheThingFactory->newObject( temp, self->getTeam() );
DEBUG_LOG(("%s Added a carriage, %s \n", self->getTemplate()->getName().str(),firstCarriage->getTemplate()->getName().str()));
}
if ( firstCarriage )
{
firstCarriage->setProducer(self);
m_trailerID = firstCarriage->getID();
static NameKeyType key_rb = NAMEKEY("RailroadBehavior");
RailroadBehavior *rb = (RailroadBehavior*)firstCarriage->findUpdateModule(key_rb);
if( rb )
{
if ( closeCarriage )
rb->hitchNewCarriagebyProximity( self->getID(), m_track );
else
rb->hitchNewCarriagebyTemplate( self->getID(), list, ++iter, m_track );// ! increment iter here!
}
else
{
DEBUG_ASSERTCRASH( rb,
("%s is attempting to hitch carriage, %s without a RailroadBehavior... \nwhat kind of nutty conductor are you? ",
self->getTemplate()->getName().str(),
firstCarriage->getTemplate()->getName().str() ) );
}
}
}
}
m_carriagesCreated = TRUE;
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::hitchNewCarriagebyTemplate( ObjectID locoID, const TemplateNameVector& list, TemplateNameIterator& iter, TrainTrack *track)
{
if ( m_isLocomotive )
{
DEBUG_ASSERTCRASH(m_isLocomotive, ("You can not hitch a locomotive in mid train, dude."));
return;
}
Object *locomotive = TheGameLogic->findObjectByID( locoID );
if ( !locomotive )// sanity
return;
m_track = track; //ALWAYS INCREFERENCE WHENEVER YOU INHERIT THIS POINTER
m_track->incReference();//ALWAYS INCREFERENCE WHENEVER YOU INHERIT THIS POINTER
m_hasEverBeenHitched = TRUE;// so no other trains try to hitch me onto them
//Okay that's me, now for the next guy
//---------------------------------------
Object *newCarriage = NULL;
if ( iter != list.end() )// this test is bogus
{
const ThingTemplate* temp = TheThingFactory->findTemplate( *iter );
if (temp)
{
newCarriage = TheThingFactory->newObject( temp, locomotive->getTeam() ); // just a little worried about this...
newCarriage->setProducer(locomotive);
m_trailerID = newCarriage->getID();
static NameKeyType key_rb = NAMEKEY("RailroadBehavior");
RailroadBehavior *rb = (RailroadBehavior*)newCarriage->findUpdateModule(key_rb);
if( rb )
{
rb->hitchNewCarriagebyTemplate( locoID, list, ++iter, m_track );
}
else
{
DEBUG_ASSERTCRASH( rb,
("%s could not hitch a %s without a RailroadBehavior... \nwhat kind of nutty conductor are you? \nThe next carriage would have been a %s.",
locomotive->getTemplate()->getName().str(),
newCarriage->getTemplate()->getName().str(),
*iter
) );
}
}
}
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::hitchNewCarriagebyProximity( ObjectID locoID, TrainTrack *track)
{
if ( m_isLocomotive )
{
DEBUG_ASSERTCRASH(m_isLocomotive, ("You can not hitch a locomotive in mid train, dude."));
return;
}
Object *locomotive = TheGameLogic->findObjectByID( locoID );
if ( !locomotive )// sanity
return;
m_track = track; //ALWAYS INCREFERENCE WHENEVER YOU INHERIT THIS POINTER
m_track->incReference();//ALWAYS INCREFERENCE WHENEVER YOU INHERIT THIS POINTER
m_hasEverBeenHitched = TRUE;// so no other trains try to hitch me onto them
//Okay that's me, now for the next guy
//---------------------------------------
//First we'll see if the map artist put some cars down on the track for us to find
const RailroadBehaviorModuleData* md = getRailroadBehaviorModuleData();
Object *self = getObject();
Real maxRadius = self->getGeometryInfo().getMajorRadius() * 2.0f;
Coord3D myHitchLoc = *self->getPosition();
Coord3D hitchOffset = *self->getUnitDirectionVector2D();//copy that
hitchOffset.scale ( - maxRadius );// negative, since I want the back, not the front
myHitchLoc.add( & hitchOffset );
PartitionFilterIsValidCarriage pfivc(self, md);
PartitionFilter *filters[] = { &pfivc, 0 };
Object* xferCarriage = NULL;
Object *closeCarriage = NULL;
if ( m_trailerID != INVALID_ID )
{
xferCarriage = TheGameLogic->findObjectByID( m_trailerID );
}
if (xferCarriage != NULL)
closeCarriage = xferCarriage;
else
closeCarriage = ThePartitionManager->getClosestObject( &myHitchLoc, maxRadius, FROM_CENTER_2D, filters);
if ( closeCarriage )
{
closeCarriage->setProducer(self);
m_trailerID = closeCarriage->getID();
static NameKeyType key_rb = NAMEKEY("RailroadBehavior");
RailroadBehavior *rb = (RailroadBehavior*)closeCarriage->findUpdateModule(key_rb);
if( rb )
rb->hitchNewCarriagebyProximity( self->getID(), m_track );
else
{
DEBUG_ASSERTCRASH( rb,
("%s is attempting to hitch carriage, %s without a RailroadBehavior... \nwhat kind of nutty conductor are you? ",
self->getTemplate()->getName().str(),
closeCarriage->getTemplate()->getName().str() ) );
}
}
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::getPulled( PullInfo *info )
{
//ENFORCE MY STATUS AS A PULLEE, NOT A PULLER, and update my position, speed etc.
m_wantsToBeLeadCarraige = 0;
if ( ! m_track )
{
return;
}
conductorPullInfo = *info;//copy my puller to my conductor, so that if I ever get detached
//( meaning that if this function stops getting called ), I will take over as the lead carriage!
//Wheeeee!!
info->previousWaypoint = info->currentWaypoint;
updatePositionTrackDistance( info, &m_pullInfo );
//get pointer to my trailer
Object *trailer = TheGameLogic->findObjectByID( m_trailerID );
if ( trailer )
{
//call his pull trailer with MY info;
static NameKeyType key_RGUpdate = NAMEKEY("RailroadBehavior");
RailroadBehavior *RGUpdate = (RailroadBehavior*)trailer->findUpdateModule(key_RGUpdate);
if( RGUpdate )
{
RGUpdate->getPulled( &m_pullInfo ); //< this is MY info, not the one passed to me
}
}
else
{
m_trailerID = INVALID_ID;// so I will forget about my trailer and designate myself as the caboose
if ( m_endOfLine )
TheGameLogic->destroyObject( getObject() );
}
}
// ------------------------------------------------------------------------------------------------
void alignToTerrain( Real angle, const Coord3D& pos, const Coord3D& normal, Matrix3D& mtx)
{
Coord3D x, y, z;
z = normal;
x.x = Cos( angle );
x.y = Sin( angle );
x.z = 0.0f;
if (z.z != 0.0f)
{
x.z = -(x.x*z.x + x.y*z.y) / z.z;
x.normalize();
}
DEBUG_ASSERTCRASH(fabs(x.x*z.x + x.y*z.y + x.z*z.z)<0.0001,("dot is not zero\n"));
// now computing the y vector is trivial.
y.crossProduct( &z, &x, &y );
y.normalize();
mtx.Set( x.x, y.x, z.x, pos.x,
x.y, y.y, z.y, pos.y,
x.z, y.z, z.z, pos.z );
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::updatePositionTrackDistance( PullInfo *pullerInfo, PullInfo *myInfo )
{
if ( ! m_track )
{
return;
}
Object *obj = BehaviorModule::getObject();
if ( ! obj )
return; //sanity
// IF THIS HAS BEEN CALLED, AM AM GETTING PULLED BY SOMETHING,
// my conductor, or another car
Real hitchRadius = obj->getGeometryInfo().getMajorRadius();
myInfo->trackDistance = pullerInfo->trackDistance - (hitchRadius * 2);
myInfo->speed = pullerInfo->speed;// YES, if I am getting pulled, I must obey puller
myInfo->m_direction = pullerInfo->m_direction;// YES, if I am getting pulled, I must obey puller
// pullerInfo->previousWaypoint = pullerInfo->currentWaypoint;// to feed edge test in update
// I THINK THE TURN_TO POINT SHOULD BE A BONE ON THE PREVIOUS CAR!!!!!!
FindPosByPathDistance( &myInfo->towHitchPosition,
myInfo->trackDistance, // the look ahead
m_track->m_length);
Coord3D carPosition;
FindPosByPathDistance( &carPosition,
myInfo->trackDistance,
m_track->m_length, TRUE);
Coord3D turnPos = *obj->getPosition();
if (!m_inTunnel)
turnPos.z = TheTerrainLogic->getGroundHeight( turnPos.x, turnPos.y );
const Coord3D* dir = obj->getUnitDirectionVector2D();
turnPos.x += dir->x * -hitchRadius;
turnPos.y += dir->y * -hitchRadius;
Coord3D trackPosDelta;
trackPosDelta.x = carPosition.x - turnPos.x;
trackPosDelta.y = carPosition.y - turnPos.y;
trackPosDelta.z = 0;
Real dx = pullerInfo->towHitchPosition.x - turnPos.x;
Real dy = pullerInfo->towHitchPosition.y - turnPos.y;
Real desiredAngle = atan2(dy, dx);
Real relAngle = stdAngleDiff(desiredAngle, obj->getTransformMatrix()->Get_Z_Rotation());
Matrix3D mtx;
Matrix3D tmp(1);
tmp.Translate(turnPos.x, turnPos.y, 0);
tmp.Translate(trackPosDelta.x, trackPosDelta.y, 0);
tmp.In_Place_Pre_Rotate_Z(relAngle );
tmp.Translate(-turnPos.x, -turnPos.y, 0);
mtx.mul(tmp, *obj->getTransformMatrix());
//enforce ground elevation
Coord3D normal ;
Real enforceElevation = TheTerrainLogic->getGroundHeight( turnPos.x, turnPos.y, &normal );
obj->setTransformMatrix(&mtx);
if (!m_inTunnel)
obj->setPositionZ( enforceElevation );
obj->handlePartitionCellMaintenance();
}
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
void RailroadBehavior::destroyTheWholeTrainNow( void )
{
TheGameLogic->destroyObject( getObject());
Object *trailer = TheGameLogic->findObjectByID( m_trailerID );
if ( trailer )
{
static NameKeyType key_RGUpdate = NAMEKEY("RailroadBehavior");
RailroadBehavior *RGUpdate = (RailroadBehavior*)trailer->findUpdateModule(key_RGUpdate);
if( RGUpdate )
{
RGUpdate->destroyTheWholeTrainNow(); //< this is MY info, not the one passed to me
}
}
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::FindPosByPathDistance( Coord3D *pos, const Real dist, const Real length, Bool setState )
{
if ( ! m_track )
return;
m_waitingInWings = FALSE;
Real actualDistance = dist;// WRAP TO NORMALIZE
if ( m_track->m_isLooping )
{
while ( actualDistance < 0.0f )
{
actualDistance += length;
}
while ( actualDistance > length )
{
actualDistance -= length;
}
}
else
{
if ( dist < 0 )
{
actualDistance = 0;
m_waitingInWings = TRUE;
}
else if ( dist >= length )
{
actualDistance = length;
m_endOfLine = TRUE;
}
actualDistance = MAX(MIN(length,dist),0); // CLAMP TO NORMALIZE
}
pos->set( 0.0f, 0.0f, 0.0f );
const TrackPointList *pointList = m_track->getPointList();//read-only
if ( ! pointList )
return;
std::list::const_iterator pointIter = pointList->begin();
while ( pointIter != pointList->end() )
{
const TrackPoint *thisPoint = &(*pointIter);
++pointIter;// next pointIter in this list, so then...
const TrackPoint *nextPoint = &(*pointIter);
if (thisPoint && thisPoint->m_distanceFromFirst < actualDistance)// I am after this point, and
{
Coord3D thisPointPos = thisPoint->m_position;
if (nextPoint && nextPoint->m_distanceFromFirst > actualDistance)
{
//I am between this point and the next
const Int handleFound = ((TrackPoint*)thisPoint)->getHandle();
Bool edge = (m_currentPointHandle != handleFound);
if ( setState )
{
m_inTunnel = thisPoint->m_isTunnelOrBridge;
if ( m_isLocomotive )// since only locomotives care about such things
{
// THE LOCOMOTIVE SNIFFS THE TRACK TO SMELL FOR THE NEXT STATION AND TUNNEL
if ( edge )
{
m_currentPointHandle = handleFound;
if ( thisPoint->m_isStation )
{
m_conductorState = APPLY_BRAKES;
m_disembark = FALSE;
TheAudio->removeAudioEvent(m_runningSound.getPlayingHandle());
}
else if ( thisPoint->m_isDisembark )
{
m_conductorState = APPLY_BRAKES;
m_disembark = TRUE;
TheAudio->removeAudioEvent(m_runningSound.getPlayingHandle());
}
else if ( thisPoint->m_isPingPong && conductorPullInfo.m_mostRecentSpecialPointHandle != handleFound )
{
conductorPullInfo.m_mostRecentSpecialPointHandle = handleFound;
m_conductorState = APPLY_BRAKES;
m_disembark = FALSE;
TheAudio->removeAudioEvent(m_runningSound.getPlayingHandle());
conductorPullInfo.m_direction = -conductorPullInfo.m_direction;
}
}
}
if ( edge && ! m_inTunnel )
{//play my clickety clack sound, `cause I just rode over a join IN the tracks
TheAudio->addAudioEvent( &m_clicketyClackSound );
m_clicketyClackSound.setPosition( getObject()->getPosition() );
m_clicketyClackSound.setVolume( (Real)conductorPullInfo.speed / 10.0f );//assumed max speed
}
}
Real difference = actualDistance - thisPoint->m_distanceFromFirst;
Coord3D delta = nextPoint->m_position;
delta.sub( &thisPointPos );
delta.normalize();
delta.scale( difference );
thisPointPos.add( &delta );
*pos = thisPointPos;//copy out
return;
}
else
*pos = thisPointPos;//copy out
}
}
//DEBUG_ASSERTCRASH(FALSE,("Railroad could not find a position on the path!"));
}
// ------------------------------------------------------------------------------------------------
/** CRC */
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::crc( Xfer *xfer )
{
// extend base class
UpdateModule::crc( xfer );
} // end crc
// ------------------------------------------------------------------------------------------------
/** Xfer method
* Version Info:
* 1: Initial version
* 2: Added... like, everything.
* 3: m_held script driven flag for hanging out
**/
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::xfer( Xfer *xfer )
{
// version
XferVersion currentVersion = 3;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
// PLEASE NOTE:
// m_track and trackDataLoaded are indeed NOT xferred,
// since these are inited afresh within the update module,
if ( version >= 2 )
{
// extend base class
PhysicsBehavior::xfer( xfer );
//StationTask m_nextStationTask;
xfer->xferUser( &m_nextStationTask, sizeof( StationTask ) );
//ObjectID m_trailerID; ///< the ID of the object I am directly pulling
xfer->xferObjectID( &m_trailerID );
//Int m_currentPointHandle;
xfer->xferInt( &m_currentPointHandle );
//Int m_waitAtStationTimer;
xfer->xferInt( &m_waitAtStationTimer );
//Bool m_carriagesCreated; ///< TRUE once we have made all the cars in the train
xfer->xferBool( &m_carriagesCreated );
//Bool m_hasEverBeenHitched; /// has somebody ever hitched me? Remains true, even after puller dies.
xfer->xferBool( &m_hasEverBeenHitched );
//Bool m_waitingInWings; /// I have not entered the real track yet, so leave me alone
xfer->xferBool( &m_waitingInWings );
//Bool m_endOfLine; /// I have reached the end of a non looping track
xfer->xferBool( &m_endOfLine );
//Bool m_isLocomotive; ///< Am I a locomotive,
xfer->xferBool( &m_isLocomotive );
//Bool m_isLeadCarraige; ///< Am the carraige in front,
xfer->xferBool( &m_isLeadCarraige );
//Int m_wantsToBeLeadCarraige; ///< Am the carraige in front,
xfer->xferInt( &m_wantsToBeLeadCarraige );
//Bool m_disembark; ///< If I wait at a station, I should also evacuate everybody when I get theres
xfer->xferBool( &m_disembark );
//Bool m_inTunnel; ///< Am I in a tunnel, so I wil not snap to ground height, until the next waypoint,
xfer->xferBool( &m_inTunnel );
//ConductorState m_conductorState;
xfer->xferUser( &m_conductorState, sizeof( ConductorState ) );
xfer->xferUser( &m_anchorWaypointID, sizeof( WaypointID ) );
//PullInfo m_pullInfo;
m_pullInfo.xferPullInfo( xfer );
//PullInfo conductorPullInfo;
conductorPullInfo.xferPullInfo( xfer );
}
if( version >= 3 )
{
xfer->xferBool( &m_held );
}
} // end xfer
void RailroadBehavior::PullInfo::xferPullInfo( Xfer *xfer )
{
XferVersion currentVersion = 1;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
xfer->xferReal( &m_direction);
xfer->xferReal( &speed);
xfer->xferReal( &trackDistance);
xfer->xferCoord3D( &towHitchPosition );
xfer->xferInt( &m_mostRecentSpecialPointHandle );
xfer->xferUnsignedInt( &previousWaypoint );
xfer->xferUnsignedInt( ¤tWaypoint );
}
// ------------------------------------------------------------------------------------------------
/** Load post process */
// ------------------------------------------------------------------------------------------------
void RailroadBehavior::loadPostProcess( void )
{
// extend base class
PhysicsBehavior::loadPostProcess();
//Bool m_trackDataLoaded; ///< have I TRIED to load track data, yet? I only try once!
m_trackDataLoaded = FALSE;
const RailroadBehaviorModuleData *modData = getRailroadBehaviorModuleData();
m_runningSound = modData->m_runningSound;
m_clicketyClackSound = modData->m_clicketyClackSound;
m_whistleSound = modData->m_whistleSound;
m_runningSound.setObjectID( getObject()->getID() );
m_whistleSound.setObjectID( getObject()->getID() ) ;
m_clicketyClackSound.setObjectID( getObject()->getID() ) ;
} // end loadPostProcess