mirror of https://github.com/GTAmodding/re3.git
Merge 8cabe529bf
into ae1deb2209
This commit is contained in:
commit
e8c8b3ff94
|
@ -2347,11 +2347,17 @@ PlayCruising:
|
|||
SampleManager.StopChannel(CHANNEL_PLAYER_VEHICLE_ENGINE);
|
||||
if (isMoped || accelerateState >= 150 && wheelsOnGround && brakeState <= 0 && !params.m_pVehicle->bIsHandbrakeOn
|
||||
&& !lostTraction && currentGear >= params.m_pTransmission->nNumberOfGears - 1) {
|
||||
if (accelerateState >= 220 && params.m_fVelocityChange + 0.001f >= velocityChangeForAudio) {
|
||||
if (nCruising < 800)
|
||||
++nCruising;
|
||||
} else if (nCruising > 3) {
|
||||
--nCruising;
|
||||
#ifdef FIX_BUGS
|
||||
// Prevent the fake top gear ("cruise gear") rising in pitch too quickly at high FPS.
|
||||
if (CTimer::GetLogicalFramesPassed())
|
||||
#endif
|
||||
{
|
||||
if (accelerateState >= 220 && params.m_fVelocityChange + 0.001f >= velocityChangeForAudio) {
|
||||
if (nCruising < 800)
|
||||
++nCruising;
|
||||
} else if (nCruising > 3) {
|
||||
--nCruising;
|
||||
}
|
||||
}
|
||||
freq = 27 * nCruising + freqModifier + 22050;
|
||||
if (engineSoundType == SFX_BANK_TRUCK)
|
||||
|
|
|
@ -3895,10 +3895,12 @@ CCam::Process_Debug(const CVector&, float, float, float)
|
|||
if(Alpha > DEGTORAD(89.5f)) Alpha = DEGTORAD(89.5f);
|
||||
else if(Alpha < DEGTORAD(-89.5f)) Alpha = DEGTORAD(-89.5f);
|
||||
|
||||
const float TimeStep = CTimer::GetTimeStepNonClipped();
|
||||
|
||||
if(CPad::GetPad(1)->GetSquare() || KEYDOWN('W'))
|
||||
Speed += 0.1f;
|
||||
Speed += 0.1f * TimeStep;
|
||||
else if(CPad::GetPad(1)->GetCross() || KEYDOWN('S'))
|
||||
Speed -= 0.1f;
|
||||
Speed -= 0.1f * TimeStep;
|
||||
else
|
||||
Speed = 0.0f;
|
||||
if(Speed > 70.0f) Speed = 70.0f;
|
||||
|
@ -3906,9 +3908,9 @@ CCam::Process_Debug(const CVector&, float, float, float)
|
|||
|
||||
|
||||
if(KEYDOWN(rsRIGHT) || KEYDOWN('D'))
|
||||
PanSpeedX += 0.1f;
|
||||
PanSpeedX += 0.1f * TimeStep;
|
||||
else if(KEYDOWN(rsLEFT) || KEYDOWN('A'))
|
||||
PanSpeedX -= 0.1f;
|
||||
PanSpeedX -= 0.1f * TimeStep;
|
||||
else
|
||||
PanSpeedX = 0.0f;
|
||||
if(PanSpeedX > 70.0f) PanSpeedX = 70.0f;
|
||||
|
@ -3916,23 +3918,22 @@ CCam::Process_Debug(const CVector&, float, float, float)
|
|||
|
||||
|
||||
if(KEYDOWN(rsUP))
|
||||
PanSpeedY += 0.1f;
|
||||
PanSpeedY += 0.1f * TimeStep;
|
||||
else if(KEYDOWN(rsDOWN))
|
||||
PanSpeedY -= 0.1f;
|
||||
PanSpeedY -= 0.1f * TimeStep;
|
||||
else
|
||||
PanSpeedY = 0.0f;
|
||||
if(PanSpeedY > 70.0f) PanSpeedY = 70.0f;
|
||||
if(PanSpeedY < -70.0f) PanSpeedY = -70.0f;
|
||||
|
||||
|
||||
Front = TargetCoors - Source;
|
||||
Front.Normalise();
|
||||
Source = Source + Front*Speed;
|
||||
Source = Source + Front * Speed * TimeStep;
|
||||
|
||||
Up = CVector{ 0.0f, 0.0f, 1.0f };
|
||||
CVector Right = CrossProduct(Front, Up);
|
||||
Up = CrossProduct(Right, Front);
|
||||
Source = Source + Up*PanSpeedY + Right*PanSpeedX;
|
||||
Source = Source + Up * PanSpeedY * TimeStep + Right * PanSpeedX * TimeStep;
|
||||
|
||||
if(Source.z < -450.0f)
|
||||
Source.z = -450.0f;
|
||||
|
@ -3955,11 +3956,7 @@ CCam::Process_Debug(const CVector&, float, float, float)
|
|||
Source.y += 1.0f;
|
||||
GetVectorsReadyForRW();
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_CAMERA);
|
||||
#else
|
||||
CPad::GetPad(0)->DisablePlayerControls = PLAYERCONTROL_CAMERA;
|
||||
#endif
|
||||
|
||||
if(CPad::GetPad(1)->GetLeftShockJustDown() && gbBigWhiteDebugLightSwitchedOn)
|
||||
CShadows::StoreShadowToBeRendered(SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &Source,
|
||||
|
|
|
@ -590,7 +590,12 @@ CPhysical::ApplyAirResistance(void)
|
|||
}else if(GetStatus() != STATUS_GHOST){
|
||||
float f = Pow(1.0f/Abs(1.0f + m_fAirResistance*0.5f*m_vecMoveSpeed.MagnitudeSqr()), CTimer::GetTimeStep());
|
||||
m_vecMoveSpeed *= f;
|
||||
#ifdef FIX_BUGS
|
||||
// Fix too much friction at high FPS (evil cause of bad vehicle handling, rear tires unable to lose traction, etc!)
|
||||
m_vecTurnSpeed *= Pow(0.99f, CTimer::GetTimeStepFix());
|
||||
#else
|
||||
m_vecTurnSpeed *= 0.99f;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1071,6 +1076,10 @@ CPhysical::ApplyFriction(CPhysical *B, float adhesiveLimit, CColPoint &colpoint)
|
|||
if(fOtherSpeedA > speedSum){
|
||||
impulseA = (speedSum - fOtherSpeedA) * A->m_fMass;
|
||||
impulseB = (speedSum - fOtherSpeedB) * B->m_fMass;
|
||||
#ifdef FIX_BUGS
|
||||
impulseA *= CTimer::GetTimeStepFix();
|
||||
impulseB *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep();
|
||||
if(impulseA < -impulseLimit) impulseA = -impulseLimit;
|
||||
#ifdef FIX_BUGS
|
||||
|
@ -1107,6 +1116,10 @@ CPhysical::ApplyFriction(CPhysical *B, float adhesiveLimit, CColPoint &colpoint)
|
|||
if(fOtherSpeedA > speedSum){
|
||||
impulseA = (speedSum - fOtherSpeedA) * A->m_fMass;
|
||||
impulseB = (speedSum - fOtherSpeedB) * massB;
|
||||
#ifdef FIX_BUGS
|
||||
impulseA *= CTimer::GetTimeStepFix();
|
||||
impulseB *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep();
|
||||
if(impulseA < -impulseLimit) impulseA = -impulseLimit;
|
||||
if(impulseB > impulseLimit) impulseB = impulseLimit;
|
||||
|
@ -1140,6 +1153,10 @@ CPhysical::ApplyFriction(CPhysical *B, float adhesiveLimit, CColPoint &colpoint)
|
|||
if(fOtherSpeedA > speedSum){
|
||||
impulseA = (speedSum - fOtherSpeedA) * massA;
|
||||
impulseB = (speedSum - fOtherSpeedB) * B->m_fMass;
|
||||
#ifdef FIX_BUGS
|
||||
impulseA *= CTimer::GetTimeStepFix();
|
||||
impulseB *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep();
|
||||
if(impulseA < -impulseLimit) impulseA = -impulseLimit;
|
||||
if(impulseB > impulseLimit) impulseB = impulseLimit;
|
||||
|
@ -1174,6 +1191,10 @@ CPhysical::ApplyFriction(CPhysical *B, float adhesiveLimit, CColPoint &colpoint)
|
|||
if(fOtherSpeedA > speedSum){
|
||||
impulseA = (speedSum - fOtherSpeedA) * massA;
|
||||
impulseB = (speedSum - fOtherSpeedB) * massB;
|
||||
#ifdef FIX_BUGS
|
||||
impulseA *= CTimer::GetTimeStepFix();
|
||||
impulseB *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep();
|
||||
if(impulseA < -impulseLimit) impulseA = -impulseLimit;
|
||||
if(impulseB > impulseLimit) impulseB = impulseLimit;
|
||||
|
@ -1214,6 +1235,9 @@ CPhysical::ApplyFriction(float adhesiveLimit, CColPoint &colpoint)
|
|||
// not really impulse but speed
|
||||
// maybe use ApplyFrictionMoveForce instead?
|
||||
fImpulse = -fOtherSpeed;
|
||||
#ifdef FIX_BUGS
|
||||
fImpulse *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep() / m_fMass;
|
||||
if(fImpulse < -impulseLimit) fImpulse = -impulseLimit;
|
||||
CVector vImpulse = frictionDir*fImpulse;
|
||||
|
@ -1235,6 +1259,9 @@ CPhysical::ApplyFriction(float adhesiveLimit, CColPoint &colpoint)
|
|||
frictionDir = vOtherSpeed * (1.0f/fOtherSpeed);
|
||||
#endif
|
||||
fImpulse = -fOtherSpeed * m_fMass;
|
||||
#ifdef FIX_BUGS
|
||||
fImpulse *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
impulseLimit = adhesiveLimit*CTimer::GetTimeStep() * 1.5;
|
||||
if(fImpulse < -impulseLimit) fImpulse = -impulseLimit;
|
||||
ApplyFrictionMoveForce(frictionDir*fImpulse);
|
||||
|
@ -1834,9 +1861,16 @@ CPhysical::ProcessCollisionSectorList(CPtrList *lists)
|
|||
A->GetStatus() == STATUS_PLAYER && A->IsVehicle() &&
|
||||
Abs(A->m_vecMoveSpeed.x) > 0.2f &&
|
||||
Abs(A->m_vecMoveSpeed.y) > 0.2f){
|
||||
#ifdef FIX_BUGS
|
||||
// Fix vehicles having lower turning circles at high FPS
|
||||
A->m_vecMoveFriction.x += CTimer::GetTimeStepFix() * moveSpeed.x * -0.3f / numCollisions;
|
||||
A->m_vecMoveFriction.y += CTimer::GetTimeStepFix() * moveSpeed.y * -0.3f / numCollisions;
|
||||
A->m_vecTurnFriction += CTimer::GetTimeStepFix() * turnSpeed * -0.3f / numCollisions;
|
||||
#else
|
||||
A->m_vecMoveFriction.x += moveSpeed.x * -0.3f / numCollisions;
|
||||
A->m_vecMoveFriction.y += moveSpeed.y * -0.3f / numCollisions;
|
||||
A->m_vecTurnFriction += turnSpeed * -0.3f / numCollisions;
|
||||
#endif
|
||||
}
|
||||
|
||||
if(B->IsObject() && Bobj->m_nCollisionDamageEffect && maxImpulseA > 20.0f)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "Timer.h"
|
||||
#include "rtcharse.h"
|
||||
#include "re3_inttypes.h"
|
||||
#include "Frontend.h"
|
||||
#include "debugmenu.h"
|
||||
#include <new>
|
||||
|
||||
|
@ -1017,6 +1018,10 @@ DebugMenuProcess(void)
|
|||
CPad *pad = CPad::GetPad(0);
|
||||
if(CTRLJUSTDOWN('M'))
|
||||
menuOn = !menuOn;
|
||||
if (KEYJUSTDOWN(rsF4))
|
||||
FrontEndMenuManager.m_PrefsFrameLimiter = !FrontEndMenuManager.m_PrefsFrameLimiter;
|
||||
if(KEYJUSTDOWN(rsESC))
|
||||
menuOn = false;
|
||||
if(!menuOn)
|
||||
return;
|
||||
|
||||
|
@ -1309,4 +1314,4 @@ DebugMenuEntrySetAddress(MenuEntry *e, void *addr)
|
|||
((MenuEntry_Float32*)e)->variable = (float*)addr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
|
@ -429,6 +429,12 @@ CParticleObject::RemoveObject(void)
|
|||
void
|
||||
CParticleObject::UpdateAll(void)
|
||||
{
|
||||
#ifdef FIX_BUGS
|
||||
// Fix particle generation spam at high FPS
|
||||
if (CTimer::GetLogicalFramesPassed() == 0)
|
||||
return;
|
||||
#endif
|
||||
|
||||
{
|
||||
CParticleObject *pobj = pCloseListHead;
|
||||
CParticleObject *nextpobj;
|
||||
|
|
|
@ -1259,8 +1259,11 @@ void CParticle::Update()
|
|||
}
|
||||
|
||||
vecPos += vecMoveStep;
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
if ( psystem->m_Type == PARTICLE_FIREBALL && CTimer::GetLogicalFramesPassed()) // Fix particle spam at high FPS
|
||||
#else
|
||||
if ( psystem->m_Type == PARTICLE_FIREBALL )
|
||||
#endif
|
||||
{
|
||||
AddParticle(PARTICLE_HEATHAZE, particle->m_vecPosition, CVector(0.0f, 0.0f, 0.0f),
|
||||
nil, particle->m_fSize * 5.0f);
|
||||
|
@ -1326,11 +1329,18 @@ void CParticle::Update()
|
|||
|
||||
fDistToCam = (TheCamera.GetPosition() - vecPos).Magnitude();
|
||||
}
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
if ( numWaterDropOnScreen < nMaxDrops && numWaterDropOnScreen < 63
|
||||
&& fDistToCam < 10.0f
|
||||
&& clearWaterDrop == false
|
||||
&& !CGame::IsInInterior()
|
||||
&& CTimer::GetLogicalFramesPassed()) // Fix waterdrop spam at high FPS
|
||||
#else
|
||||
if ( numWaterDropOnScreen < nMaxDrops && numWaterDropOnScreen < 63
|
||||
&& fDistToCam < 10.0f
|
||||
&& clearWaterDrop == false
|
||||
&& !CGame::IsInInterior() )
|
||||
#endif
|
||||
{
|
||||
CVector vecWaterdropTarget
|
||||
(
|
||||
|
@ -1411,7 +1421,11 @@ void CParticle::Update()
|
|||
}
|
||||
}
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
if ( !(psystem->Flags & SCREEN_TRAIL) && CTimer::GetLogicalFramesPassed()) // Fix particle over-expansion at high FPS
|
||||
#else
|
||||
if ( !(psystem->Flags & SCREEN_TRAIL) )
|
||||
#endif
|
||||
{
|
||||
float size;
|
||||
|
||||
|
@ -1701,6 +1715,12 @@ void CParticle::Update()
|
|||
}
|
||||
}
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Keep particles animating, rotating, fading etc at the right speed
|
||||
// at high or low FPS.
|
||||
for (uint32 i=0; i<CTimer::GetLogicalFramesPassed(); i++)
|
||||
#endif
|
||||
{ // -- start FPS fix ---
|
||||
if ( particle->m_nFadeToBlackTimer != 0 )
|
||||
{
|
||||
particle->m_nColorIntensity = Clamp(particle->m_nColorIntensity - particle->m_nFadeToBlackTimer,
|
||||
|
@ -1764,6 +1784,7 @@ void CParticle::Update()
|
|||
#else
|
||||
particle->m_nRotation += particle->m_nRotationStep;
|
||||
#endif
|
||||
} // -- end FPS fix --
|
||||
|
||||
if ( particle->m_fCurrentZRadius != 0.0f )
|
||||
{
|
||||
|
@ -1773,6 +1794,16 @@ void CParticle::Update()
|
|||
|
||||
float fY = (Sin(nSinCosIndex) + Cos(nSinCosIndex)) * particle->m_fCurrentZRadius;
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Prevent super-fast movement at high FPS
|
||||
// We can use GetTimeSetpFix() here instead of GetLogicalFramesPassed()
|
||||
// so the visual result looks "smooth" on player's screens. We can't
|
||||
// do this with most of the above Timers because the effects are stored
|
||||
// as uint16's instead of floats, which means tiny changes each frame
|
||||
// are often rounded down to zero change each frame.
|
||||
fX *= CTimer::GetTimeStepFix();
|
||||
fY *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
vecPos -= particle->m_vecParticleMovementOffset;
|
||||
|
||||
vecPos += CVector(fX, fY, 0.0f);
|
||||
|
|
|
@ -81,7 +81,7 @@ enum tParticleType
|
|||
PARTICLE_TEST,
|
||||
PARTICLE_BIRD_FRONT,
|
||||
PARTICLE_SHIP_SIDE,
|
||||
PARTICLE_BEASTIE,
|
||||
PARTICLE_BEASTIE, // "Beasties" look like tree leaves circling up at canopy height, but are also used in other places (eg docks)
|
||||
PARTICLE_RAINDROP_2D,
|
||||
PARTICLE_HEATHAZE,
|
||||
PARTICLE_HEATHAZE_IN_DIST,
|
||||
|
|
|
@ -921,16 +921,30 @@ CWaterLevel::RenderWater()
|
|||
|
||||
if ( !CTimer::GetIsPaused() )
|
||||
{
|
||||
TEXTURE_ADDU += windAddUV;
|
||||
TEXTURE_ADDV += windAddUV;
|
||||
|
||||
_TEXTURE_MASK_ADDU += Sin(fAngle) * 0.0005f + 1.1f * windAddUV;
|
||||
_TEXTURE_MASK_ADDV -= Cos(fAngle * 1.3f) * 0.0005f + 1.2f * windAddUV;
|
||||
|
||||
_TEXTURE_WAKE_ADDU -= Sin(fAngle) * 0.0003f + windAddUV;
|
||||
_TEXTURE_WAKE_ADDV += Cos(fAngle * 0.7f) * 0.0003f + windAddUV;
|
||||
#ifdef FIX_BUGS
|
||||
// Stop the movement of the ocean speeding up at high FPS.
|
||||
// Should be purely aesthetic, affecting only the texture UVs.
|
||||
TEXTURE_ADDU += windAddUV * CTimer::GetTimeStepFix();
|
||||
TEXTURE_ADDV += windAddUV * CTimer::GetTimeStepFix();
|
||||
|
||||
_TEXTURE_MASK_ADDU += (Sin(fAngle) * 0.0005f + 1.1f * windAddUV) * CTimer::GetTimeStepFix();
|
||||
_TEXTURE_MASK_ADDV -= (Cos(fAngle * 1.3f) * 0.0005f + 1.2f * windAddUV) * CTimer::GetTimeStepFix();
|
||||
|
||||
_TEXTURE_WAKE_ADDU -= (Sin(fAngle) * 0.0003f + windAddUV) * CTimer::GetTimeStepFix();
|
||||
_TEXTURE_WAKE_ADDV += (Cos(fAngle * 0.7f) * 0.0003f + windAddUV) * CTimer::GetTimeStepFix();
|
||||
#else
|
||||
TEXTURE_ADDU += windAddUV;
|
||||
TEXTURE_ADDV += windAddUV;
|
||||
|
||||
_TEXTURE_MASK_ADDU += Sin(fAngle) * 0.0005f + 1.1f * windAddUV;
|
||||
_TEXTURE_MASK_ADDV -= Cos(fAngle * 1.3f) * 0.0005f + 1.2f * windAddUV;
|
||||
|
||||
_TEXTURE_WAKE_ADDU -= Sin(fAngle) * 0.0003f + windAddUV;
|
||||
_TEXTURE_WAKE_ADDV += Cos(fAngle * 0.7f) * 0.0003f + windAddUV;
|
||||
#endif
|
||||
}
|
||||
|
||||
// What does this code do? Are the above equations sometimes unstable and make big numbers?
|
||||
if ( _TEXTURE_MASK_ADDU >= 1.0f )
|
||||
_TEXTURE_MASK_ADDU = 0.0f;
|
||||
if ( _TEXTURE_MASK_ADDV >= 1.0f )
|
||||
|
@ -1954,7 +1968,7 @@ CWaterLevel::RenderWavyMask(float fX, float fY, float fZ,
|
|||
#ifndef PC_WATER
|
||||
if (maskMorphVerts[base].z >= fMinSparkZ)
|
||||
#else
|
||||
if ( maskMorphVerts[base].z > fMinSparkZ )
|
||||
if (maskMorphVerts[base].z > fMinSparkZ)
|
||||
#endif
|
||||
{
|
||||
switch ( (i + j + randval) & 3 )
|
||||
|
|
|
@ -169,7 +169,11 @@ void CWeather::Update(void)
|
|||
LightningFlash = false;
|
||||
LightningBurst = false;
|
||||
}
|
||||
#ifdef FIX_BUGS
|
||||
else if (CTimer::GetLogicalFramesPassed()) {
|
||||
#else
|
||||
else{
|
||||
#endif
|
||||
if (LightningBurst) {
|
||||
if ((CGeneral::GetRandomNumber() & 255) >= 32) {
|
||||
// 0.875 probability
|
||||
|
@ -303,7 +307,11 @@ void CWeather::Update(void)
|
|||
TrafficLightBrightness = Max(Foggyness, TrafficLightBrightness);
|
||||
TrafficLightBrightness = Max(Rain, TrafficLightBrightness);
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
if (CTimer::GetLogicalFramesPassed()) AddRain();
|
||||
#else
|
||||
AddRain();
|
||||
#endif
|
||||
|
||||
if ((NewWeatherType == WEATHER_SUNNY || NewWeatherType == WEATHER_EXTRA_SUNNY) &&
|
||||
!CGame::IsInInterior() && !CCutsceneMgr::IsRunning() && (CTimer::GetFrameCounter() & 7) == 0) {
|
||||
|
@ -323,6 +331,11 @@ void CWeather::Update(void)
|
|||
|
||||
void CWeather::AddHeatHaze()
|
||||
{
|
||||
#ifdef FIX_BUGS
|
||||
// Fix particle spam at high FPS
|
||||
if (!CTimer::GetLogicalFramesPassed())
|
||||
return;
|
||||
#endif
|
||||
if(TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOPDOWN ||
|
||||
TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED)
|
||||
return;
|
||||
|
@ -338,6 +351,11 @@ void CWeather::AddHeatHaze()
|
|||
|
||||
void CWeather::AddBeastie()
|
||||
{
|
||||
#ifdef FIX_BUGS
|
||||
// Fix particle spam at high FPS. Beasties look like tree leaves.
|
||||
if (!CTimer::GetLogicalFramesPassed())
|
||||
return;
|
||||
#endif
|
||||
if(FindPlayerVehicle() || CTimer::GetFrameCounter()%10 || (CGeneral::GetRandomNumber()&5) == 0)
|
||||
return;
|
||||
CVector pos = TheCamera.GetPosition();
|
||||
|
|
|
@ -235,6 +235,12 @@ CAutomobile::CAutomobile(int32 id, uint8 CreatedBy)
|
|||
bExplosionProof = true;
|
||||
bBulletProof = true;
|
||||
}
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Probably not neccesary to zero these
|
||||
m_nCarHornTimer = 0;
|
||||
m_fCarHornTimeButtonLastHit = 0.0f;
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -857,8 +863,12 @@ CAutomobile::ProcessControl(void)
|
|||
(m_aSuspensionSpringRatio[1] < 1.0f || m_aSuspensionSpringRatio[3] < 1.0f))
|
||||
ApplyTurnForce(-GRAVITY*Min(m_fTurnMass, 2500.0f)*GetUp(), -1.0f*GetForward());
|
||||
}
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Keep brake non-timestepped (so the later ProcessWheel() takes only non-timestepped inputs)
|
||||
brake = m_fBrakePedal * pHandling->fBrakeDeceleration * CTimer::GetDefaultTimeStep();
|
||||
#else
|
||||
brake = m_fBrakePedal * pHandling->fBrakeDeceleration * CTimer::GetTimeStep();
|
||||
#endif
|
||||
bool neutralHandling = GetStatus() != STATUS_PLAYER && GetStatus() != STATUS_PLAYER_REMOTE && (pHandling->Flags & HANDLING_NEUTRALHANDLING);
|
||||
float brakeBiasFront = neutralHandling ? 1.0f : 2.0f*pHandling->fBrakeBias;
|
||||
float brakeBiasRear = neutralHandling ? 1.0f : 2.0f-pHandling->fBrakeBias; // looks like a bug, but it was correct in III...
|
||||
|
@ -1058,12 +1068,7 @@ CAutomobile::ProcessControl(void)
|
|||
float rearBrake = brake;
|
||||
float rearTraction = traction;
|
||||
if(bIsHandbrakeOn){
|
||||
#ifdef FIX_BUGS
|
||||
// Not sure if this is needed, but brake usually has timestep as a factor
|
||||
rearBrake = 20000.0f * CTimer::GetTimeStepFix();
|
||||
#else
|
||||
rearBrake = 20000.0f;
|
||||
#endif
|
||||
if(fwdSpeed > 0.1f && pHandling->Flags & HANDLING_HANDBRAKE_TYRE){
|
||||
m_fTireTemperature += 0.005*CTimer::GetTimeStep();
|
||||
if(m_fTireTemperature > 2.0f)
|
||||
|
@ -1072,8 +1077,11 @@ CAutomobile::ProcessControl(void)
|
|||
}else if(m_doingBurnout && mod_HandlingManager.HasRearWheelDrive(pHandling->nIdentifier)){
|
||||
rearBrake = 0.0f;
|
||||
rearTraction = 0.0f;
|
||||
// BUG: missing timestep
|
||||
#ifdef FIX_BUGS
|
||||
ApplyTurnForce(contactPoints[CARWHEEL_REAR_LEFT], -0.001f*m_fTurnMass*m_fSteerAngle*GetRight()*CTimer::GetTimeStepFix());
|
||||
#else
|
||||
ApplyTurnForce(contactPoints[CARWHEEL_REAR_LEFT], -0.001f*m_fTurnMass*m_fSteerAngle*GetRight());
|
||||
#endif
|
||||
}else if(m_fTireTemperature > 1.0f){
|
||||
rearTraction *= m_fTireTemperature;
|
||||
}
|
||||
|
@ -1364,18 +1372,49 @@ CAutomobile::ProcessControl(void)
|
|||
ReduceHornCounter();
|
||||
}else{
|
||||
if(UsesSiren()){
|
||||
#ifdef FIX_BUGS
|
||||
// Allow sirens to be toggled at high FPS
|
||||
const float minPressTime = 100.0f; // milli-seconds
|
||||
bool currentButtonState = Pads[0].bHornHistory[(CPad::HORNHISTORY_SIZE + Pads[0].iCurrHornHistory - 0) % CPad::HORNHISTORY_SIZE];
|
||||
bool lastButtonState = Pads[0].bHornHistory[(CPad::HORNHISTORY_SIZE + Pads[0].iCurrHornHistory - 1) % CPad::HORNHISTORY_SIZE]; // Extra addition of CPad::HORNHISTORY_SIZE avoids modulo of negative numbers
|
||||
|
||||
if (currentButtonState && !lastButtonState)
|
||||
{
|
||||
// Horn button has just been hit
|
||||
m_fCarHornTimeButtonLastHit = CTimer::GetTimeInMilliseconds();
|
||||
}
|
||||
else if (currentButtonState && lastButtonState)
|
||||
{
|
||||
// Horn button is being held down
|
||||
if (CTimer::GetTimeInMilliseconds() - m_fCarHornTimeButtonLastHit >= minPressTime)
|
||||
m_nCarHornTimer = 1; // enable horn
|
||||
}
|
||||
else if (!currentButtonState && lastButtonState)
|
||||
{
|
||||
// Horn button has just been released
|
||||
m_nCarHornTimer = 0;
|
||||
if (CTimer::GetTimeInMilliseconds() - m_fCarHornTimeButtonLastHit < minPressTime)
|
||||
m_bSirenOrAlarm = !m_bSirenOrAlarm; // Toggle siren-like-feature
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing pressed
|
||||
m_nCarHornTimer = 0; // Should not be neccesary, but may as well keep in
|
||||
}
|
||||
#else
|
||||
if(Pads[0].bHornHistory[Pads[0].iCurrHornHistory]){
|
||||
if(Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+4) % 5] &&
|
||||
Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+3) % 5])
|
||||
if(Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+4) % CPad::HORNHISTORY_SIZE] &&
|
||||
Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+3) % CPad::HORNHISTORY_SIZE])
|
||||
m_nCarHornTimer = 1;
|
||||
else
|
||||
m_nCarHornTimer = 0;
|
||||
}else if(Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+4) % 5] &&
|
||||
!Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+1) % 5]){
|
||||
}else if(Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+4) % CPad::HORNHISTORY_SIZE] &&
|
||||
!Pads[0].bHornHistory[(Pads[0].iCurrHornHistory+1) % CPad::HORNHISTORY_SIZE]){
|
||||
m_nCarHornTimer = 0;
|
||||
m_bSirenOrAlarm = !m_bSirenOrAlarm;
|
||||
}else
|
||||
m_nCarHornTimer = 0;
|
||||
#endif
|
||||
}else if(GetModelIndex() != MI_VOODOO && !CVehicle::bCheat3 && !carHasNitro){
|
||||
if(!IsAlarmOn()){
|
||||
if(Pads[0].GetHorn())
|
||||
|
@ -5738,8 +5777,14 @@ CAutomobile::ShowAllComps(void)
|
|||
void
|
||||
CAutomobile::ReduceHornCounter(void)
|
||||
{
|
||||
#ifdef FIX_BUGS
|
||||
// Make horns last longer (only used by AI drivers?)
|
||||
if(m_nCarHornTimer != 0 && CTimer::GetLogicalFramesPassed())
|
||||
m_nCarHornTimer--;
|
||||
#else
|
||||
if(m_nCarHornTimer != 0)
|
||||
m_nCarHornTimer--;
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -722,7 +722,12 @@ CBike::ProcessControl(void)
|
|||
acceleration = pHandling->Transmission.CalculateDriveAcceleration(m_fGasPedal, m_nCurrentGear, m_fChangeGearTime, fwdSpeed, gripCheat);
|
||||
acceleration /= m_fForceMultiplier;
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Keep timestep out of this, it gets added properly inside ProcessWheel() later. Fixes weak brakes at high FPS.
|
||||
brake = m_fBrakePedal * pHandling->fBrakeDeceleration * CTimer::GetDefaultTimeStep();
|
||||
#else
|
||||
brake = m_fBrakePedal * pHandling->fBrakeDeceleration * CTimer::GetTimeStep();
|
||||
#endif
|
||||
bool neutralHandling = GetStatus() != STATUS_PLAYER && GetStatus() != STATUS_PLAYER_REMOTE && (pHandling->Flags & HANDLING_NEUTRALHANDLING);
|
||||
float brakeBiasFront = neutralHandling ? 1.0f : 2.0f*pHandling->fBrakeBias;
|
||||
float brakeBiasRear = neutralHandling ? 1.0f : 2.0f*(1.0f-pHandling->fBrakeBias);
|
||||
|
@ -882,12 +887,7 @@ CBike::ProcessControl(void)
|
|||
wheelRight.Normalise();
|
||||
|
||||
if(bIsHandbrakeOn){
|
||||
#ifdef FIX_BUGS
|
||||
// Not sure if this is needed, but brake usually has timestep as a factor
|
||||
rearBrake = 20000.0f * CTimer::GetTimeStepFix();
|
||||
#else
|
||||
rearBrake = 20000.0f;
|
||||
#endif
|
||||
m_fTireTemperature = 1.0f;
|
||||
}else if(m_doingBurnout){
|
||||
rearBrake = 0.0f;
|
||||
|
@ -2941,8 +2941,14 @@ CBike::SetupModelNodes(void)
|
|||
void
|
||||
CBike::ReduceHornCounter(void)
|
||||
{
|
||||
if(m_nCarHornTimer != 0)
|
||||
#ifdef FIX_BUGS
|
||||
// Make horns last longer (only used by AI drivers?)
|
||||
if(m_nCarHornTimer != 0 && CTimer::GetLogicalFramesPassed())
|
||||
m_nCarHornTimer--;
|
||||
#else
|
||||
if(m_nCarHornTimer != 0)
|
||||
m_nCarHornTimer--;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef COMPATIBLE_SAVES
|
||||
|
|
|
@ -263,9 +263,17 @@ CBoat::ProcessControl(void)
|
|||
}
|
||||
|
||||
// Damage particles
|
||||
#ifdef FIX_BUGS
|
||||
if(m_fHealth <= 460.0f && GetStatus() != STATUS_WRECKED &&
|
||||
Abs(GetPosition().x - TheCamera.GetPosition().x) < 200.0f &&
|
||||
Abs(GetPosition().y - TheCamera.GetPosition().y) < 200.0f &&
|
||||
CTimer::GetLogicalFramesPassed() ) // Fix high-FPS particle spam
|
||||
{
|
||||
#else
|
||||
if(m_fHealth <= 460.0f && GetStatus() != STATUS_WRECKED &&
|
||||
Abs(GetPosition().x - TheCamera.GetPosition().x) < 200.0f &&
|
||||
Abs(GetPosition().y - TheCamera.GetPosition().y) < 200.0f){
|
||||
#endif
|
||||
float speedSq = m_vecMoveSpeed.MagnitudeSqr();
|
||||
CVector smokeDir = 0.8f*m_vecMoveSpeed;
|
||||
CVector smokePos;
|
||||
|
@ -335,6 +343,7 @@ CBoat::ProcessControl(void)
|
|||
bIsDrowning = false;
|
||||
}
|
||||
|
||||
// Apply buoyancy impulse the first time (why twice?)
|
||||
m_fVolumeUnderWater = mod_Buoyancy.m_volumeUnderWater;
|
||||
m_vecBuoyancePoint = buoyancePoint;
|
||||
if(GetModelIndex() == MI_SKIMMER && GetUp().z < -0.5f && Abs(m_vecMoveSpeed.x) < 0.2f && Abs(m_vecMoveSpeed.y) < 0.2f)
|
||||
|
@ -344,13 +353,17 @@ CBoat::ProcessControl(void)
|
|||
if(bSeparateTurnForce)
|
||||
ApplyTurnForce(0.4f*buoyanceImpulse, buoyancePoint);
|
||||
|
||||
// TODO: what is this?
|
||||
if(GetModelIndex() == MI_SKIMMER)
|
||||
if(m_skimmerThingTimer != 0.0f ||
|
||||
GetForward().z < -0.5f && GetUp().z > -0.5f && m_vecMoveSpeed.z < -0.15f &&
|
||||
buoyanceImpulse.z > 0.01f*m_fMass * GRAVITY*CTimer::GetTimeStep() &&
|
||||
buoyanceImpulse.z < 0.4f*m_fMass * GRAVITY*CTimer::GetTimeStep()){
|
||||
#ifdef FIX_BUGS
|
||||
// buoyanceImpulse already has a factor of timestep in it
|
||||
float turnImpulse = -0.00017f*GetForward().z*buoyanceImpulse.z * m_fMass*CTimer::GetDefaultTimeStep();
|
||||
#else
|
||||
float turnImpulse = -0.00017f*GetForward().z*buoyanceImpulse.z * m_fMass*CTimer::GetTimeStep();
|
||||
#endif
|
||||
ApplyTurnForce(turnImpulse*GetForward(), GetUp());
|
||||
bBoatInWater = false;
|
||||
//BUG? aren't we forgetting the timestep here?
|
||||
|
@ -362,15 +375,21 @@ CBoat::ProcessControl(void)
|
|||
m_skimmerThingTimer = 0.0f;
|
||||
}
|
||||
|
||||
// Apply buoyancy impulse the second time (why twice?)
|
||||
if(!onLand && bBoatInWater && GetUp().z > 0.0f){
|
||||
#ifdef FIX_BUGS
|
||||
// buoyanceImpulse already has a factor of timestep in it
|
||||
float impulse = m_vecMoveSpeed.MagnitudeSqr()*pBoatHandling->fAqPlaneForce*buoyanceImpulse.z*CTimer::GetDefaultTimeStep()*0.5f;
|
||||
#else
|
||||
float impulse = m_vecMoveSpeed.MagnitudeSqr()*pBoatHandling->fAqPlaneForce*buoyanceImpulse.z*CTimer::GetTimeStep()*0.5f;
|
||||
#endif
|
||||
if(GetModelIndex() == MI_SKIMMER)
|
||||
impulse *= 1.0f + m_fGasPedal;
|
||||
else if(m_fGasPedal > 0.05f)
|
||||
impulse *= m_fGasPedal;
|
||||
else
|
||||
impulse = 0.0f;
|
||||
impulse = Min(impulse, GRAVITY*pBoatHandling->fAqPlaneLimit*m_fMass*CTimer::GetTimeStep());
|
||||
impulse = Min(impulse, GRAVITY*pBoatHandling->fAqPlaneLimit*m_fMass*CTimer::GetTimeStep()); // Both sides have a factor of TimeStep, therefore this Min() is not a problem at high FPS
|
||||
ApplyMoveForce(impulse*GetUp());
|
||||
ApplyTurnForce(impulse*GetUp(), buoyancePoint - pBoatHandling->fAqPlaneOffset*GetForward());
|
||||
}
|
||||
|
@ -378,7 +397,11 @@ CBoat::ProcessControl(void)
|
|||
// Handle boat moving forward
|
||||
float fwdSpeed = 1.0f;
|
||||
if(Abs(m_fGasPedal) > 0.05f || (fwdSpeed = m_vecMoveSpeed.Magnitude2D()) > 0.01f){
|
||||
#ifdef FIX_BUGS
|
||||
if(bBoatInWater && fwdSpeed > 0.05f && CTimer::GetLogicalFramesPassed()) // Fix super-short wake trail at high FPS
|
||||
#else
|
||||
if(bBoatInWater && fwdSpeed > 0.05f)
|
||||
#endif
|
||||
AddWakePoint(GetPosition());
|
||||
|
||||
float steerFactor = 1.0f;
|
||||
|
@ -433,7 +456,11 @@ CBoat::ProcessControl(void)
|
|||
|
||||
// Spray some particles
|
||||
CVector jetDir = -0.04f * force;
|
||||
#ifdef FIX_BUGS
|
||||
if(m_fGasPedal > 0.0f && CTimer::GetLogicalFramesPassed()){ // Fix high-FPS particle spam
|
||||
#else
|
||||
if(m_fGasPedal > 0.0f){
|
||||
#endif
|
||||
if(GetStatus() == STATUS_PLAYER){
|
||||
CVector sternPos = GetColModel()->boundingBox.min;
|
||||
sternPos.x = 0.0f;
|
||||
|
@ -475,7 +502,11 @@ CBoat::ProcessControl(void)
|
|||
CVector propellerForce = propellerDepth * Multiply3x3(GetMatrix(), force*CVector(-steerSin, 0.0f, 0.0f));
|
||||
ApplyMoveForce(propellerForce * CTimer::GetTimeStep());
|
||||
ApplyTurnForce(propellerForce * CTimer::GetTimeStep(), propeller);
|
||||
#ifdef FIX_BUGS
|
||||
float rightForce = -steerSin * force * 0.75f/steerFactor * CTimer::GetTimeStep();
|
||||
#else
|
||||
float rightForce = -steerSin * force * 0.75f/steerFactor * Max(CTimer::GetTimeStep(), 0.01f);
|
||||
#endif
|
||||
ApplyTurnForce(GetRight() * rightForce, GetUp());
|
||||
}
|
||||
}else
|
||||
|
@ -485,7 +516,12 @@ CBoat::ProcessControl(void)
|
|||
CVector right = CrossProduct(GetForward(), CVector(0.0f, 0.0f, 1.0f));
|
||||
float rightSpeed = DotProduct(m_vecMoveSpeed, right);
|
||||
float impulse = 0.1f*pHandling->fSuspensionBias * m_fMass * m_fVolumeUnderWater * rightSpeed * CTimer::GetTimeStep();
|
||||
ApplyMoveForce(right - impulse * 0.3f * CVector(-right.y, right.x, 0.0f));
|
||||
#ifdef FIX_BUGS
|
||||
// Fix boat perf at high FPS: ensure both terms have a (correct) factor of timestep before doing subtraction
|
||||
ApplyMoveForce(right * CTimer::GetTimeStepFix() - impulse * 0.3f * CVector(-right.y, right.x, 0.0f));
|
||||
#else
|
||||
ApplyMoveForce(right - impulse * 0.3f * CVector(-right.y, right.x, 0.0f));
|
||||
#endif
|
||||
}
|
||||
|
||||
if(GetStatus() == STATUS_PLAYER && CPad::GetPad(0)->GetHandBrake()){
|
||||
|
@ -503,25 +539,45 @@ CBoat::ProcessControl(void)
|
|||
m_vecMoveSpeed.y = Min(m_vecMoveSpeed.y, -(GetPosition().y - (WORLD_MAX_Y-100.0f))*0.01f); // north
|
||||
m_vecMoveSpeed.y = Max(m_vecMoveSpeed.y, -(GetPosition().y - (WORLD_MIN_Y+100.0f))*0.01f); // south
|
||||
|
||||
// Apply water resistance to linear movement
|
||||
if(!onLand && bBoatInWater && !bSeparateTurnForce)
|
||||
ApplyWaterResistance();
|
||||
|
||||
// Apply water resistance to rotation
|
||||
if((GetModelIndex() != MI_SKIMMER || m_skimmerThingTimer == 0.0f) && !bSeparateTurnForce){
|
||||
// No idea what exactly is going on here besides drag in YZ
|
||||
#ifdef FIX_BUGS
|
||||
// Rockstar's attempts to make this framerate independent are totally wrong.
|
||||
// Rules of thumb:
|
||||
// - use Pow(x,time) if you multiply the result into the velocity
|
||||
// - use x*time if you add the result into the velocity
|
||||
// We have to disable one of these Pow() methods and then add our own correction at the end.
|
||||
float fxfake = Pow(pBoatHandling->vecTurnRes.x, CTimer::GetDefaultTimeStep());
|
||||
float fy = Pow(pBoatHandling->vecTurnRes.y, CTimer::GetTimeStep());
|
||||
float fz = Pow(pBoatHandling->vecTurnRes.z, CTimer::GetTimeStep());
|
||||
m_vecTurnSpeed = Multiply3x3(m_vecTurnSpeed, GetMatrix()); // invert - to local space
|
||||
float drag = 1.0f/(1000.0f * SQR(m_vecTurnSpeed.x) + 1.0f) * fxfake;
|
||||
m_vecTurnSpeed.y *= fy;
|
||||
m_vecTurnSpeed.z *= fz;
|
||||
float forceUp = -(1.0f - drag) * m_vecTurnSpeed.x * m_fTurnMass;
|
||||
m_vecTurnSpeed = Multiply3x3(GetMatrix(), m_vecTurnSpeed); // back to world
|
||||
CVector com = Multiply3x3(GetMatrix(), m_vecCentreOfMass);
|
||||
ApplyTurnForce(forceUp*GetUp() * CTimer::GetTimeStepFix(), com + GetForward());
|
||||
#else
|
||||
float fx = Pow(pBoatHandling->vecTurnRes.x, CTimer::GetTimeStep());
|
||||
float fy = Pow(pBoatHandling->vecTurnRes.y, CTimer::GetTimeStep());
|
||||
float fz = Pow(pBoatHandling->vecTurnRes.z, CTimer::GetTimeStep());
|
||||
m_vecTurnSpeed = Multiply3x3(m_vecTurnSpeed, GetMatrix()); // invert - to local space
|
||||
// TODO: figure this out
|
||||
float magic = 1.0f/(1000.0f * SQR(m_vecTurnSpeed.x) + 1.0f) * fx;
|
||||
float drag = 1.0f/(1000.0f * SQR(m_vecTurnSpeed.x) + 1.0f) * fx;
|
||||
m_vecTurnSpeed.y *= fy;
|
||||
m_vecTurnSpeed.z *= fz;
|
||||
float forceUp = (magic - 1.0f) * m_vecTurnSpeed.x * m_fTurnMass;
|
||||
float forceUp = (drag - 1.0f) * m_vecTurnSpeed.x * m_fTurnMass;
|
||||
m_vecTurnSpeed = Multiply3x3(GetMatrix(), m_vecTurnSpeed); // back to world
|
||||
CVector com = Multiply3x3(GetMatrix(), m_vecCentreOfMass);
|
||||
ApplyTurnForce(forceUp*GetUp(), com + GetForward());
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
m_nDeltaVolumeUnderWater = (m_fVolumeUnderWater-m_fPrevVolumeUnderWater)*10000;
|
||||
|
||||
// Falling into water
|
||||
|
@ -539,6 +595,9 @@ CBoat::ProcessControl(void)
|
|||
speedFwd *= -m_nDeltaVolumeUnderWater * 0.01f * pHandling->fBrakeBias;
|
||||
CVector speed = speedFwd*GetForward() + CVector(0.0f, 0.0f, speedUp);
|
||||
CVector splashImpulse = speed * m_fMass;
|
||||
#ifdef FIX_BUGS
|
||||
splashImpulse *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
ApplyMoveForce(splashImpulse);
|
||||
ApplyTurnForce(splashImpulse, buoyancePoint);
|
||||
}
|
||||
|
@ -546,8 +605,15 @@ CBoat::ProcessControl(void)
|
|||
|
||||
// Splashes
|
||||
float speed = m_vecMoveSpeed.Magnitude();
|
||||
#ifdef FIX_BUGS
|
||||
if(speed > 0.05f && GetUp().x > 0.0f && !TheCamera.GetLookingForwardFirstPerson() && IsVisible() &&
|
||||
(AutoPilot.m_nCarMission != MISSION_CRUISE || (CTimer::GetFrameCounter()&2) == 0)){
|
||||
(AutoPilot.m_nCarMission != MISSION_CRUISE || (CTimer::GetFrameCounter()&2) == 0) &&
|
||||
CTimer::GetLogicalFramesPassed() ) // Fix particle spam at high FPS
|
||||
#else
|
||||
if(speed > 0.05f && GetUp().x > 0.0f && !TheCamera.GetLookingForwardFirstPerson() && IsVisible() &&
|
||||
(AutoPilot.m_nCarMission != MISSION_CRUISE || (CTimer::GetFrameCounter()&2) == 0))
|
||||
#endif
|
||||
{
|
||||
CVector splashPos, splashDir;
|
||||
float splashSize, front, waterLevel;
|
||||
|
||||
|
@ -682,8 +748,14 @@ CBoat::ProcessControl(void)
|
|||
}
|
||||
|
||||
// Spray waterdrops on screen
|
||||
#ifdef FIX_BUGS
|
||||
if(TheCamera.GetLookingForwardFirstPerson() && FindPlayerVehicle() && FindPlayerVehicle()->IsBoat() &&
|
||||
m_nDeltaVolumeUnderWater > 0 && numWaterDropOnScreen < 20){
|
||||
m_nDeltaVolumeUnderWater > 0 && numWaterDropOnScreen < 20 && CTimer::GetLogicalFramesPassed()) // Fix particle spam at high FPS
|
||||
#else
|
||||
if(TheCamera.GetLookingForwardFirstPerson() && FindPlayerVehicle() && FindPlayerVehicle()->IsBoat() &&
|
||||
m_nDeltaVolumeUnderWater > 0 && numWaterDropOnScreen < 20)
|
||||
#endif
|
||||
{
|
||||
CVector dropPos;
|
||||
CVector dropDir(CGeneral::GetRandomNumberInRange(-0.25f, 0.25f), CGeneral::GetRandomNumberInRange(1.0f, 0.75f), 0.0f);
|
||||
|
||||
|
@ -714,7 +786,13 @@ CBoat::ProcessControl(void)
|
|||
numWaterDropOnScreen++;
|
||||
}
|
||||
|
||||
if(m_fPrevVolumeUnderWater == 0.0f && m_fVolumeUnderWater > 0.0f && GetModelIndex() == MI_SKIMMER){
|
||||
#ifdef FIX_BUGS
|
||||
if(m_fPrevVolumeUnderWater == 0.0f && m_fVolumeUnderWater > 0.0f && GetModelIndex() == MI_SKIMMER &&
|
||||
CTimer::GetLogicalFramesPassed()) // Fix particle spam at high FPS
|
||||
#else
|
||||
if(m_fPrevVolumeUnderWater == 0.0f && m_fVolumeUnderWater > 0.0f && GetModelIndex() == MI_SKIMMER)
|
||||
#endif
|
||||
{
|
||||
CVector splashDir(0.0f, 0.0f, 0.25f*speed);
|
||||
CVector splashPos = GetPosition();
|
||||
float level;
|
||||
|
@ -773,6 +851,18 @@ CBoat::ProcessControlInputs(uint8 pad)
|
|||
m_fBrake += (CPad::GetPad(pad)->GetBrake()/255.0f - m_fBrake)*0.1f;
|
||||
m_fBrake = Clamp(m_fBrake, 0.0f, 1.0f);
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Fix accelerator control and steering control ramp rates at high FPS
|
||||
if(m_fBrake < 0.05f){
|
||||
m_fBrake = 0.0f;
|
||||
m_fAccelerate += (CPad::GetPad(pad)->GetAccelerate()/255.0f - m_fAccelerate)*0.1f*CTimer::GetTimeStepFix();
|
||||
m_fAccelerate = Clamp(m_fAccelerate, 0.0f, 1.0f);
|
||||
}else
|
||||
m_fAccelerate = -m_fBrake*0.3f;
|
||||
|
||||
m_fSteeringLeftRight += (-CPad::GetPad(pad)->GetSteeringLeftRight()/128.0f - m_fSteeringLeftRight)*0.2f*CTimer::GetTimeStepFix();
|
||||
m_fSteeringLeftRight = Clamp(m_fSteeringLeftRight, -1.0f, 1.0f);
|
||||
#else
|
||||
if(m_fBrake < 0.05f){
|
||||
m_fBrake = 0.0f;
|
||||
m_fAccelerate += (CPad::GetPad(pad)->GetAccelerate()/255.0f - m_fAccelerate)*0.1f;
|
||||
|
@ -782,6 +872,7 @@ CBoat::ProcessControlInputs(uint8 pad)
|
|||
|
||||
m_fSteeringLeftRight += (-CPad::GetPad(pad)->GetSteeringLeftRight()/128.0f - m_fSteeringLeftRight)*0.2f;
|
||||
m_fSteeringLeftRight = Clamp(m_fSteeringLeftRight, -1.0f, 1.0f);
|
||||
#endif
|
||||
|
||||
float steeringSq = m_fSteeringLeftRight < 0.0f ? -SQR(m_fSteeringLeftRight) : SQR(m_fSteeringLeftRight);
|
||||
m_fSteerAngle = pHandling->fSteeringLock * DEGTORAD(steeringSq);
|
||||
|
@ -793,6 +884,82 @@ float fSeaPlaneWaterResistance = 30.0f;
|
|||
void
|
||||
CBoat::ApplyWaterResistance(void)
|
||||
{
|
||||
#ifdef FIX_BUGS
|
||||
// TODO: confirm if the explanation below makes sense
|
||||
float depthResistance = 0.001f * pHandling->fSuspensionForceLevel * SQR(m_fVolumeUnderWater) * m_fMass;
|
||||
if(GetModelIndex() == MI_SKIMMER)
|
||||
depthResistance *= fSeaPlaneWaterResistance;
|
||||
float fwdSpeed = DotProduct(GetMoveSpeed(), GetForward());
|
||||
|
||||
// Water resistances goes up with the square of boat speed (in real life it goes up by the
|
||||
// cube? close enough!). An extra 0.05f fudge factor was probably put in to make sure the
|
||||
// boat still has resistance at low speeds (ie auto-brakes to standstill).
|
||||
float waterResistance = (SQR(fwdSpeed) + 0.05f) * Abs(depthResistance); // Abs() used defensively, negative numbers stuff things up later
|
||||
// waterResistance will now be a small number like 0.002 or 0.015
|
||||
|
||||
// An odd use of Abs() was in the original binary. It's possible that the developers did not
|
||||
// put this in intentionally, instead the compiler may have silently added it to avoid
|
||||
// Pow() having to deal with negative numbers to a fractional power (undefined) later.
|
||||
// Regardless it was done badly, making assumptions like vecMoveRes never accidentally
|
||||
// being negative, so the use of Abs() has changed a little bit in this FIX_BUGS. In
|
||||
// real gameplay these corner cases should rarely (never?) be encountered anyway.
|
||||
|
||||
// Our boat has different water resistances when travelling forwards (y axis) versus
|
||||
// sideways (x axis). Boats tend to find it hard to move sideways.
|
||||
float rx = Abs(pBoatHandling->vecMoveRes.x/(waterResistance + 1.0f)); // Abs() used defensively, negative numbers stuff things up later
|
||||
float ry = Abs(pBoatHandling->vecMoveRes.y/(waterResistance + 1.0f));
|
||||
float rz = Abs(pBoatHandling->vecMoveRes.z/(waterResistance + 1.0f));
|
||||
// These rx, ry, rz resistance numbers will each be something like 0.8 or 0.9 or so
|
||||
|
||||
// Fun fact: the above equations are _approximately_ the same as:
|
||||
//
|
||||
// pBoatHandling->vecMoveRes.x * (1.0f - waterResistance)
|
||||
//
|
||||
// If you change the equations to this then boating feels about the same in-game.
|
||||
// Which version makes more sense compared to physics in real life? Probably neither :P
|
||||
// This second version of the equation is a little more efficient however (no division).
|
||||
|
||||
// Now how do we apply these rx ry rz resistance numbers to the boat speed?
|
||||
// It's not simple:
|
||||
//
|
||||
// - We can't multiply them into the speed once per frame, because then players with
|
||||
// higher framerates will get a lot more friction when boating (lower top speed).
|
||||
//
|
||||
// - We can't linearly modify each r number based off frametime, as higher FPS players
|
||||
// will still end up with more friction. This is for the same reason why linearly
|
||||
// reducing your bank account's yearly interest into monthly amounts but then
|
||||
// compounding it monthly will yield you more money than just compounding it yearly.
|
||||
//
|
||||
// - We could try compounding each r number based off how many fixed units of time have
|
||||
// passed (eg multiply itself by itself for every 1ms elapsed this frame). This will
|
||||
// work fairly regardless of framerate.
|
||||
//
|
||||
// We don't actually have to limit ourselves to a fixed time unit (like 1ms chunks),
|
||||
// instead we can raise the resistance to some power of time using Pow().
|
||||
float rrx = Pow(rx, 0.5f*CTimer::GetTimeStep()); // Why 0.5f? Taste?
|
||||
float rry = Pow(ry, 0.5f*CTimer::GetTimeStep());
|
||||
float rrz = Pow(rz, 0.5f*CTimer::GetTimeStep());
|
||||
float rryfake = Pow(ry, 0.5f*CTimer::GetDefaultTimeStep()); // Fudge factor needed for other equations to make sense at high FPS
|
||||
|
||||
m_vecMoveSpeed = Multiply3x3(m_vecMoveSpeed, GetMatrix()); // convert velocities to boat-local space (y = boat forwards, x = sideways, z = up/down)
|
||||
m_vecMoveSpeed.x *= rrx;
|
||||
m_vecMoveSpeed.y *= rry;
|
||||
m_vecMoveSpeed.z *= rrz;
|
||||
float force = (rryfake - 1.0f) * m_vecMoveSpeed.y * m_fMass;
|
||||
m_vecMoveSpeed = Multiply3x3(GetMatrix(), m_vecMoveSpeed); // back to world space
|
||||
|
||||
// WTH is this for?
|
||||
ApplyTurnForce(force*GetForward()*CTimer::GetTimeStepFix(), -GetUp());
|
||||
|
||||
// What the hell? Why arbitrarily compound in one more factor of rrz?
|
||||
// This is framerate dependent! Bah! I suspect it has very little effect.
|
||||
/*
|
||||
if(m_vecMoveSpeed.z > 0.0f)
|
||||
m_vecMoveSpeed.z *= rrz;
|
||||
else
|
||||
m_vecMoveSpeed.z *= (1.0f - rrz)*0.5f + rrz;
|
||||
*/
|
||||
#else
|
||||
// TODO: figure out how this works
|
||||
float resistance = 0.001f * pHandling->fSuspensionForceLevel * SQR(m_fVolumeUnderWater) * m_fMass;
|
||||
if(GetModelIndex() == MI_SKIMMER)
|
||||
|
@ -817,6 +984,7 @@ CBoat::ApplyWaterResistance(void)
|
|||
m_vecMoveSpeed.z *= fz;
|
||||
else
|
||||
m_vecMoveSpeed.z *= (1.0f - fz)*0.5f + fz;
|
||||
#endif
|
||||
}
|
||||
|
||||
RwObject*
|
||||
|
|
|
@ -109,6 +109,17 @@ cBuoyancy::ProcessBuoyancyBoat(CVehicle *veh, float buoyancy, CVector *point, CV
|
|||
float volume = SimpleSumBuoyancyData(waterLevel, waterPosition);
|
||||
float upImpulse = volume * volDiv * buoyancy * CTimer::GetTimeStep();
|
||||
CVector speed = veh->GetSpeed(Multiply3x3(veh->GetMatrix(), CVector(x, y, 0.0f)));
|
||||
#ifdef FIX_BUGS
|
||||
// "GetSpeed" seems to depend on framerate (bad).
|
||||
// This ruins boat performance at high FPS. Approximate sequence of events:
|
||||
// - "speed" goes tiny at high FPS
|
||||
// - "damp" goes high as a result
|
||||
// - high dampening reduces the buoyancy forces that keep the boat above the water
|
||||
// - boats sit _slightly_ lower in water (you have to disable ALL waves to see this, it's a very small change)
|
||||
// - all of boat handling performance changes because a different amount of the boat is underwater
|
||||
// Finding this was a PITA!
|
||||
speed /= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
float damp = 1.0f - DotProduct(speed, waterNormal)*veh->pHandling->fSuspensionDampingLevel;
|
||||
float finalImpulse = upImpulse*Max(damp, 0.0f);
|
||||
impulse->z += finalImpulse;
|
||||
|
|
|
@ -55,6 +55,8 @@ cTransmission::CalculateGearForSimpleCar(float speed, uint8 &gear)
|
|||
}
|
||||
}
|
||||
|
||||
/* This function causes 'pulsing' of throttle when reversing at max speed. You
|
||||
* can most easily observe this when the framelimiter is enabled.*/
|
||||
float
|
||||
cTransmission::CalculateDriveAcceleration(const float &gasPedal, uint8 &gear, float &time, const float &velocity, bool cheat)
|
||||
{
|
||||
|
@ -123,7 +125,12 @@ cTransmission::CalculateDriveAcceleration(const float &gasPedal, uint8 &gear, fl
|
|||
float targetVelocity = Gears[gear].fMaxVelocity*speedMul*fCheat;
|
||||
float accel = (targetVelocity - fVelocity) * (fEngineAcceleration*accelMul) / Abs(targetVelocity);
|
||||
if(Abs(fVelocity) < Abs(Gears[gear].fMaxVelocity*fCheat))
|
||||
#ifdef FIX_BUGS
|
||||
// The acceleration provided by a transmission+engine should not depend on framelength.
|
||||
fAcceleration = gasPedal * accel;
|
||||
#else
|
||||
fAcceleration = gasPedal * accel * CTimer::GetTimeStep();
|
||||
#endif
|
||||
else
|
||||
fAcceleration = 0.0f;
|
||||
return fAcceleration;
|
||||
|
|
|
@ -126,6 +126,9 @@ CVehicle::CVehicle(uint8 CreatedBy)
|
|||
m_nCarHornTimer = 0;
|
||||
m_nCarHornPattern = 0;
|
||||
m_nCarHornDelay = 0;
|
||||
#ifdef FIX_BUGS
|
||||
m_fCarHornTimeButtonLastHit = 0.0f;
|
||||
#endif
|
||||
bPartOfConvoy = false;
|
||||
bHeliMinimumTilt = false;
|
||||
bAudioChangingGear = false;
|
||||
|
@ -788,7 +791,14 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
bAlreadySkidding = false;
|
||||
#endif
|
||||
|
||||
// how much force we want to apply in these axes
|
||||
// Velocity impulses fwd and right. Units are like meters per second.
|
||||
// This function was written assuming a fixed FPS, so a correction is made
|
||||
// right at the end before actually applying these impulses.
|
||||
//
|
||||
// Note that many functions in this engine deal with "force impulses" rather
|
||||
// than "velocity impulses". They are directly related: F=ma. It is
|
||||
// possible that the original devs actually used force impulses here but
|
||||
// an optimising compiler re-arranged their maths.
|
||||
float fwd = 0.0f;
|
||||
float right = 0.0f;
|
||||
|
||||
|
@ -804,7 +814,12 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
bAlreadySkidding = true;
|
||||
*wheelState = WHEEL_STATE_NORMAL;
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Everything else here is timestep independent, let's stay with this theme to keep the rest of the FPS bugfixes simpler.
|
||||
adhesion *= CTimer::GetDefaultTimeStep();
|
||||
#else
|
||||
adhesion *= CTimer::GetTimeStep();
|
||||
#endif
|
||||
if(bAlreadySkidding)
|
||||
adhesion *= pHandling->fTractionLoss;
|
||||
|
||||
|
@ -812,12 +827,6 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
if(contactSpeedRight != 0.0f){
|
||||
// exert opposing force
|
||||
right = -contactSpeedRight/wheelsOnGround;
|
||||
// BUG?
|
||||
// contactSpeedRight is independent of framerate but right has timestep as a factor
|
||||
// so we probably have to fix this
|
||||
// fixing this causes jittery cars at 15fps, and causes the car to move backwards slowly at 18fps
|
||||
// at 19fps, the effects are gone ...
|
||||
//right *= CTimer::GetTimeStepFix();
|
||||
|
||||
if(wheelStatus == WHEEL_STATUS_BURST){
|
||||
float fwdspeed = Min(contactSpeedFwd, fBurstSpeedMax);
|
||||
|
@ -828,7 +837,7 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
if(bDriving){
|
||||
fwd = thrust;
|
||||
|
||||
// limit sideways force (why?)
|
||||
// Limit sideways forces applied by the tires to the max the tires can possibly do.
|
||||
if(right > 0.0f){
|
||||
if(right > adhesion)
|
||||
right = adhesion;
|
||||
|
@ -838,11 +847,6 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
}
|
||||
}else if(contactSpeedFwd != 0.0f){
|
||||
fwd = -contactSpeedFwd/wheelsOnGround;
|
||||
#ifdef FIX_BUGS
|
||||
// contactSpeedFwd is independent of framerate but fwd has timestep as a factor
|
||||
// so we probably have to fix this
|
||||
fwd *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
|
||||
if(!bBraking){
|
||||
if(m_fGasPedal < 0.01f){
|
||||
|
@ -854,9 +858,6 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
brake = 0.2f * mod_HandlingManager.fWheelFriction / pHandling->fMass;
|
||||
else
|
||||
brake = mod_HandlingManager.fWheelFriction / pHandling->fMass;
|
||||
#ifdef FIX_BUGS
|
||||
brake *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -883,10 +884,10 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
*wheelState = WHEEL_STATE_SKIDDING;
|
||||
}
|
||||
|
||||
float l = Sqrt(speedSq);
|
||||
float speed = Sqrt(speedSq);
|
||||
float tractionLoss = bAlreadySkidding ? 1.0f : pHandling->fTractionLoss;
|
||||
right *= adhesion * tractionLoss / l;
|
||||
fwd *= adhesion * tractionLoss / l;
|
||||
right *= adhesion * tractionLoss / speed;
|
||||
fwd *= adhesion * tractionLoss / speed;
|
||||
}
|
||||
|
||||
if(fwd != 0.0f || right != 0.0f){
|
||||
|
@ -918,9 +919,19 @@ CVehicle::ProcessWheel(CVector &wheelFwd, CVector &wheelRight, CVector &wheelCon
|
|||
else
|
||||
turnDirection = direction;
|
||||
|
||||
|
||||
// Curious: there is a perfectly good ApplyMoveSpeed() function that
|
||||
// takes "velocity impulses", but instead we use the ApplyMoveForce()
|
||||
// function which takes a "force impulse" instead and then fix up the
|
||||
// difference by multiplying in the vehicle mass (F=ma). Possibly
|
||||
// evidence that an optimising compiler re-arranged the arithmetic?
|
||||
float impulse = speed*m_fMass;
|
||||
float turnImpulse = turnSpeed*GetMass(wheelContactPoint, turnDirection);
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
impulse = impulse * CTimer::GetTimeStepFix();
|
||||
turnImpulse = turnImpulse * CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
ApplyMoveForce(impulse * direction);
|
||||
ApplyTurnForce(turnImpulse * turnDirection, wheelContactPoint);
|
||||
}
|
||||
|
@ -944,7 +955,14 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
bAlreadySkidding = false;
|
||||
#endif
|
||||
|
||||
// how much force we want to apply in these axes
|
||||
// Velocity impulses fwd and right. Units are like meters per second.
|
||||
// This function was written assuming a fixed FPS, so a correction is made
|
||||
// right at the end before actually applying these impulses.
|
||||
//
|
||||
// Note that many functions in this engine deal with "force impulses" rather
|
||||
// than "velocity impulses". They are directly related: F=ma. It is
|
||||
// possible that the original devs actually used force impulses here but
|
||||
// an optimising compiler re-arranged their maths.
|
||||
float fwd = 0.0f;
|
||||
float right = 0.0f;
|
||||
|
||||
|
@ -961,7 +979,12 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
bAlreadySkidding = true;
|
||||
*wheelState = WHEEL_STATE_NORMAL;
|
||||
|
||||
#ifdef FIX_BUGS
|
||||
// Everything else here is timestep independent, let's stay with this theme to keep the rest of the FPS bugfixes simpler.
|
||||
adhesion *= CTimer::GetDefaultTimeStep();
|
||||
#else
|
||||
adhesion *= CTimer::GetTimeStep();
|
||||
#endif
|
||||
if(bAlreadySkidding)
|
||||
adhesion *= pHandling->fTractionLoss;
|
||||
|
||||
|
@ -974,14 +997,9 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
if(contactSpeedRight != 0.0f){
|
||||
// exert opposing force
|
||||
right = -contactSpeedRight/wheelsOnGround;
|
||||
#ifdef FIX_BUGS
|
||||
// contactSpeedRight is independent of framerate but right has timestep as a factor
|
||||
// so we probably have to fix this
|
||||
right *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
|
||||
if(wheelStatus == WHEEL_STATUS_BURST){
|
||||
float fwdspeed = Min(contactSpeedFwd, fBurstBikeSpeedMax);
|
||||
float fwdspeed = Min(contactSpeedFwd, fBurstBikeSpeedMax);
|
||||
right += fwdspeed * CGeneral::GetRandomNumberInRange(-fBurstBikeTyreMod, fBurstBikeTyreMod);
|
||||
}
|
||||
}
|
||||
|
@ -999,12 +1017,6 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
}
|
||||
}else if(contactSpeedFwd != 0.0f){
|
||||
fwd = -contactSpeedFwd/wheelsOnGround;
|
||||
#ifdef FIX_BUGS
|
||||
// contactSpeedFwd is independent of framerate but fwd has timestep as a factor
|
||||
// so we probably have to fix this
|
||||
fwd *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
|
||||
if(!bBraking){
|
||||
if(m_fGasPedal < 0.01f){
|
||||
if(IsBike())
|
||||
|
@ -1015,9 +1027,6 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
brake = 0.2f * mod_HandlingManager.fWheelFriction / m_fMass;
|
||||
else
|
||||
brake = mod_HandlingManager.fWheelFriction / m_fMass;
|
||||
#ifdef FIX_BUGS
|
||||
brake *= CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1068,6 +1077,10 @@ CVehicle::ProcessBikeWheel(CVector &wheelFwd, CVector &wheelRight, CVector &whee
|
|||
|
||||
float impulse = speed*m_fMass;
|
||||
float turnImpulse = speed*GetMass(wheelContactPoint, direction);
|
||||
#ifdef FIX_BUGS
|
||||
impulse = impulse * CTimer::GetTimeStepFix();
|
||||
turnImpulse = turnImpulse * CTimer::GetTimeStepFix();
|
||||
#endif
|
||||
CVector vTurnImpulse = turnImpulse * direction;
|
||||
ApplyMoveForce(impulse * direction);
|
||||
|
||||
|
|
|
@ -273,6 +273,9 @@ public:
|
|||
uint8 m_nCarHornPattern;
|
||||
bool m_bSirenOrAlarm;
|
||||
uint8 m_nCarHornDelay;
|
||||
#ifdef FIX_BUGS
|
||||
float m_fCarHornTimeButtonLastHit;
|
||||
#endif
|
||||
int8 m_comedyControlState;
|
||||
CStoredCollPoly m_aCollPolys[2]; // poly which is under front/rear part of car
|
||||
float m_fSteerInput;
|
||||
|
|
Loading…
Reference in New Issue