From a12c01e10ad92fe6f563dda997fea479f6227b1b Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Tue, 24 Mar 2020 09:56:37 +0200 Subject: [PATCH 01/70] obrstr + Debug --- src/core/Debug.cpp | 85 +++++++++++++++++++++++++++++-- src/core/Debug.h | 13 ++++- src/core/obrstr.cpp | 119 ++++++++++++++++++++++++++++++++++++++++++++ src/core/obrstr.h | 9 ++++ 4 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 src/core/obrstr.cpp create mode 100644 src/core/obrstr.h diff --git a/src/core/Debug.cpp b/src/core/Debug.cpp index b80e9959..bdcbaf04 100644 --- a/src/core/Debug.cpp +++ b/src/core/Debug.cpp @@ -1,12 +1,91 @@ +#include "common.h" #include "Debug.h" +#include "Font.h" +#include "main.h" +#include "Text.h" -int CDebug::ms_nCurrentTextLine; +bool gbDebugStuffInRelease = false; -void CDebug::DebugInitTextBuffer() +#define DEBUG_X_POS (300) +#define DEBUG_Y_POS (41) +#define DEBUG_LINE_HEIGHT (22) + +int16 CDebug::ms_nCurrentTextLine; +char CDebug::ms_aTextBuffer[MAX_LINES][MAX_STR_LEN]; + +void +CDebug::DebugInitTextBuffer() { ms_nCurrentTextLine = 0; } -void CDebug::DebugDisplayTextBuffer() +void +CDebug::DebugAddText(const char *str) { + int32 i = 0; + if (*str != '\0') { + while (i < MAX_STR_LEN) { + ms_aTextBuffer[ms_nCurrentTextLine][i++] = *(str++); + if (*str == '\0') + break; + } + } + + ms_aTextBuffer[ms_nCurrentTextLine++][i] = '\0'; + if (ms_nCurrentTextLine >= MAX_LINES) + ms_nCurrentTextLine = 0; +} + +void +CDebug::DebugDisplayTextBuffer() +{ +#ifndef MASTER + if (gbDebugStuffInRelease) + { + int32 i = 0; + int32 y = DEBUG_Y_POS; +#ifdef FIX_BUGS + CFont::SetPropOn(); + CFont::SetBackgroundOff(); + CFont::SetScale(1.0f, 1.0f); + CFont::SetCentreOff(); + CFont::SetRightJustifyOff(); + CFont::SetJustifyOn(); + CFont::SetRightJustifyWrap(0.0f); + CFont::SetBackGroundOnlyTextOff(); + CFont::SetFontStyle(FONT_BANK); +#else + // this is not even readable + CFont::SetPropOff(); + CFont::SetBackgroundOff(); + CFont::SetScale(1.0f, 1.0f); + CFont::SetCentreOff(); + CFont::SetRightJustifyOn(); + CFont::SetRightJustifyWrap(0.0f); + CFont::SetBackGroundOnlyTextOff(); + CFont::SetFontStyle(FONT_BANK); + CFont::SetPropOff(); +#endif + do { + char *line; + while (true) { + line = ms_aTextBuffer[(ms_nCurrentTextLine + i++) % MAX_LINES]; + if (*line != '\0') + break; + y += DEBUG_LINE_HEIGHT; + if (i == MAX_LINES) { + CFont::DrawFonts(); + return; + } + } + AsciiToUnicode(line, gUString); + CFont::SetColor(CRGBA(0, 0, 0, 255)); + CFont::PrintString(DEBUG_X_POS, y-1, gUString); + CFont::SetColor(CRGBA(255, 128, 128, 255)); + CFont::PrintString(DEBUG_X_POS+1, y, gUString); + y += DEBUG_LINE_HEIGHT; + } while (i != MAX_LINES); + CFont::DrawFonts(); + } +#endif } diff --git a/src/core/Debug.h b/src/core/Debug.h index 395f66af..444a0cf5 100644 --- a/src/core/Debug.h +++ b/src/core/Debug.h @@ -2,10 +2,19 @@ class CDebug { - static int ms_nCurrentTextLine; + enum + { + MAX_LINES = 15, + MAX_STR_LEN = 80, + }; + + static int16 ms_nCurrentTextLine; + static char ms_aTextBuffer[MAX_LINES][MAX_STR_LEN]; public: static void DebugInitTextBuffer(); static void DebugDisplayTextBuffer(); - + static void DebugAddText(const char *str); }; + +extern bool gbDebugStuffInRelease; diff --git a/src/core/obrstr.cpp b/src/core/obrstr.cpp new file mode 100644 index 00000000..3663d134 --- /dev/null +++ b/src/core/obrstr.cpp @@ -0,0 +1,119 @@ +#include "common.h" +#include "Debug.h" +#include "obrstr.h" + +char obrstr[128]; +char obrstr2[128]; + +void ObrInt(int32 n1) +{ + IntToStr(n1, obrstr); + CDebug::DebugAddText(obrstr); +} + +void ObrInt2(int32 n1, int32 n2) +{ + IntToStr(n1, obrstr); + strcat(obrstr, " "); + IntToStr(n2, obrstr2); + strcat(obrstr, obrstr2); + CDebug::DebugAddText(obrstr); +} + +void ObrInt3(int32 n1, int32 n2, int32 n3) +{ + IntToStr(n1, obrstr); + strcat(obrstr, " "); + IntToStr(n2, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n3, obrstr2); + strcat(obrstr, obrstr2); + CDebug::DebugAddText(obrstr); +} + +void ObrInt4(int32 n1, int32 n2, int32 n3, int32 n4) +{ + IntToStr(n1, obrstr); + strcat(obrstr, " "); + IntToStr(n2, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n3, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n4, obrstr2); + strcat(obrstr, obrstr2); + CDebug::DebugAddText(obrstr); +} + +void ObrInt5(int32 n1, int32 n2, int32 n3, int32 n4, int32 n5) +{ + IntToStr(n1, obrstr); + strcat(obrstr, " "); + IntToStr(n2, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n3, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n4, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n5, obrstr2); + strcat(obrstr, obrstr2); + CDebug::DebugAddText(obrstr); +} + +void ObrInt6(int32 n1, int32 n2, int32 n3, int32 n4, int32 n5, int32 n6) +{ + IntToStr(n1, obrstr); + strcat(obrstr, " "); + IntToStr(n2, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n3, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n4, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n5, obrstr2); + strcat(obrstr, obrstr2); + strcat(obrstr, " "); + IntToStr(n6, obrstr2); + strcat(obrstr, obrstr2); + CDebug::DebugAddText(obrstr); +} + +void IntToStr(int32 inNum, char *outStr) +{ + bool isNeg = inNum < 0; + + if (isNeg) { + inNum = -inNum; + *outStr = '-'; + } + + int16 digits = 1; + + if (inNum > 9) { + int32 _inNum = inNum; + do { + digits++; + _inNum /= 10; + } while (_inNum > 9); + } + + int32 strSize = digits; + if (isNeg) + strSize++; + + char *pStr = &outStr[strSize]; + int32 i = 0; + do { + *(pStr-- - 1) = (inNum % 10) + '0'; + inNum /= 10; + } while (++i < strSize); + outStr[strSize] = '\0'; +} \ No newline at end of file diff --git a/src/core/obrstr.h b/src/core/obrstr.h new file mode 100644 index 00000000..6838afb5 --- /dev/null +++ b/src/core/obrstr.h @@ -0,0 +1,9 @@ +#pragma once + +void ObrInt(int32 n1); +void ObrInt2(int32 n1, int32 n2); +void ObrInt3(int32 n1, int32 n2, int32 n3); +void ObrInt4(int32 n1, int32 n2, int32 n3, int32 n4); +void ObrInt5(int32 n1, int32 n2, int32 n3, int32 n4, int32 n5); +void ObrInt6(int32 n1, int32 n2, int32 n3, int32 n4, int32 n5, int32 n6); +void IntToStr(int32 inNum, char *outStr); \ No newline at end of file From 43fb59e356651950912ac1c980448a771c53d935 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Tue, 24 Mar 2020 17:24:47 +0100 Subject: [PATCH 02/70] Update Fire.h --- src/core/Fire.h | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/core/Fire.h b/src/core/Fire.h index 624bf608..9afcf1b0 100644 --- a/src/core/Fire.h +++ b/src/core/Fire.h @@ -7,18 +7,22 @@ class CFire public: bool m_bIsOngoing; bool m_bIsScriptFire; - bool m_bPropogationFlag; + bool m_bPropagationFlag; bool m_bAudioSet; CVector m_vecPos; CEntity *m_pEntity; CEntity *m_pSource; - int m_nExtinguishTime; - int m_nStartTime; - int field_20; - int field_24; + uint32 m_nExtinguishTime; + uint32 m_nStartTime; + int32 field_20; + uint32 field_24; uint32 m_nFiremenPuttingOut; - float field_2C; + float m_fStrength; + CFire(); + ~CFire(); + void ProcessFire(void); + void ReportThisFire(void); void Extinguish(void); }; @@ -27,20 +31,21 @@ class CFireManager enum { MAX_FIREMEN_ATTENDING = 2, }; - uint32 m_nTotalFires; public: + uint32 m_nTotalFires; CFire m_aFires[NUM_FIRES]; - void StartFire(CEntity *entityOnFire, CEntity *culprit, float, uint32); - void StartFire(CVector, float, uint8); + void StartFire(CVector pos, float size, bool propagation); + void StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength, bool propagation); void Update(void); - CFire *FindFurthestFire_NeverMindFireMen(CVector coors, float, float); - CFire *FindNearestFire(CVector, float*); + CFire *FindFurthestFire_NeverMindFireMen(CVector coords, float minRange, float maxRange); + CFire *FindNearestFire(CVector vecPos, float *pDistance); + CFire *GetNextFreeFire(void); uint32 GetTotalActiveFires() const; - void ExtinguishPoint(CVector, float); - int32 StartScriptFire(const CVector& pos, CEntity* culprit, float, uint8); - bool IsScriptFireExtinguish(int16); - void RemoveScriptFire(int16); + void ExtinguishPoint(CVector point, float range); + int32 StartScriptFire(const CVector &pos, CEntity *target, float strength, bool propagation); + bool IsScriptFireExtinguish(int16 index); void RemoveAllScriptFires(void); - void SetScriptFireAudio(int16, bool); + void RemoveScriptFire(int16 index); + void SetScriptFireAudio(int16 index, bool state); }; extern CFireManager &gFireManager; From 1680d11dae8c1169d756756ad44f685c7766af17 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Tue, 24 Mar 2020 17:25:14 +0100 Subject: [PATCH 03/70] Update Fire.h --- src/core/Fire.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Fire.h b/src/core/Fire.h index 9afcf1b0..89ab9a9f 100644 --- a/src/core/Fire.h +++ b/src/core/Fire.h @@ -40,7 +40,7 @@ public: CFire *FindFurthestFire_NeverMindFireMen(CVector coords, float minRange, float maxRange); CFire *FindNearestFire(CVector vecPos, float *pDistance); CFire *GetNextFreeFire(void); - uint32 GetTotalActiveFires() const; + uint32 GetTotalActiveFires(void) const; void ExtinguishPoint(CVector point, float range); int32 StartScriptFire(const CVector &pos, CEntity *target, float strength, bool propagation); bool IsScriptFireExtinguish(int16 index); From c03bae46ea4c9cc57a9b2ecf8da41f42b994bcf0 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Tue, 24 Mar 2020 18:12:53 +0100 Subject: [PATCH 04/70] Update Fire.cpp --- src/core/Fire.cpp | 436 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 420 insertions(+), 16 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index f83ad2c8..d53c8fc7 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -1,19 +1,299 @@ #include "common.h" #include "patcher.h" +#include "Vector.h" +#include "PlayerPed.h" +#include "Entity.h" +#include "PointLights.h" +#include "Particle.h" +#include "Timer.h" +#include "Vehicle.h" +#include "Shadows.h" +#include "Automobile.h" +#include "World.h" +#include "General.h" +#include "EventList.h" +#include "DamageManager.h" +#include "Ped.h" #include "Fire.h" CFireManager &gFireManager = *(CFireManager*)0x8F31D0; -WRAPPER void CFire::Extinguish(void) { EAXJMP(0x479D40); } -WRAPPER void CFireManager::Update(void) { EAXJMP(0x479310); } -WRAPPER CFire* CFireManager::FindFurthestFire_NeverMindFireMen(CVector coors, float, float) { EAXJMP(0x479430); } - -uint32 CFireManager::GetTotalActiveFires() const +CFire::CFire(void) { - return m_nTotalFires; + m_bIsOngoing = 0; + m_bIsScriptFire = 0; + m_bPropagationFlag = 1; + m_bAudioSet = 1; + m_vecPos = CVector(0.0f, 0.0f, 0.0f); + m_pEntity = 0; + m_pSource = 0; + m_nFiremenPuttingOut = 0; + m_nExtinguishTime = 0; + m_nStartTime = 0; + field_20 = 1; + field_24 = 0; + m_fStrength = 0.8f; } -CFire* CFireManager::FindNearestFire(CVector vecPos, float* pDistance) +CFire::~CFire(void) {} + +class CFire_ : public CFire { +public: + void ctor() { ::new(this) CFire(); } + void dtor() { CFire::CFire(); } +}; + +void +CFire::ProcessFire(void) +{ + float fDamagePlayer; + float fDamagePeds; + float fDamageVehicle; + CVehicle *pCurrentVehicle; + char nRandNumber; + float fGreen; + float fRed; + CVector lightpos; + CVector firePos; + CVector vecProduct; + + CEntity *pCurrentEntity = (CPed *)m_pEntity; + CPed *ped = (CPed *)pCurrentEntity; + + if (m_pEntity) { + m_vecPos = m_pEntity->GetPosition(); + + if (((CPed *)m_pEntity)->IsPed()) { + if (ped->m_pFire != this) { + Extinguish(); + return; + } + if (ped->m_nMoveState != PEDMOVE_RUN) + m_vecPos.z -= 1.0f; + + if (ped->bInVehicle && ped->m_pMyVehicle) { + if (ped->m_pMyVehicle->IsCar()) + ped->m_pMyVehicle->m_fHealth = 75.0f; + } else if (pCurrentEntity == (CPed *)FindPlayerPed()) { + fDamagePlayer = 1.2f * CTimer::GetTimeStep(); + + ((CPlayerPed *)pCurrentEntity)->InflictDamage( + (CPlayerPed *)m_pSource, WEAPONTYPE_FLAMETHROWER, + fDamagePlayer, PEDPIECE_TORSO, 0); + } else { + fDamagePeds = 1.2f * CTimer::GetTimeStep(); + + if (((CPlayerPed *)pCurrentEntity)->InflictDamage( + (CPlayerPed *)m_pSource, WEAPONTYPE_FLAMETHROWER, + fDamagePeds, PEDPIECE_TORSO, 0)) + + pCurrentEntity->bRenderScorched = true; + } + } else if (pCurrentEntity->IsVehicle()) { + CVehicle *pCurrentVehicle = (CVehicle*)m_pEntity; + + if (pCurrentVehicle->m_pCarFire != this) { + Extinguish(); + return; + } + if (!m_bIsScriptFire) { + fDamageVehicle = 1.2f * CTimer::GetTimeStep(); + pCurrentVehicle->InflictDamage((CVehicle *)m_pSource, WEAPONTYPE_FLAMETHROWER, fDamageVehicle); + } + } + } + if (!FindPlayerVehicle() && !FindPlayerPed()->m_pFire && !(FindPlayerPed()->bFireProof) + && ((FindPlayerPed()->GetPosition() - m_vecPos).MagnitudeSqr() < 2.0f)) { + FindPlayerPed()->DoStuffToGoOnFire(); + gFireManager.StartFire(FindPlayerPed(), m_pSource, 0.8f, 1); + } + if (CTimer::m_snTimeInMilliseconds > field_24) { /* set to 0 when a newfire starts, related to time */ + field_24 = CTimer::m_snTimeInMilliseconds + 80; + firePos = m_vecPos; + pCurrentVehicle = (CVehicle *)m_pEntity; + + if (pCurrentVehicle && pCurrentVehicle->IsVehicle() && (pCurrentVehicle->IsCar())) { + CVehicleModelInfo *mi = ((CVehicleModelInfo*)CModelInfo::GetModelInfo(pCurrentVehicle->GetModelIndex())); + CVector ModelInfo = mi->m_positions[CAR_POS_HEADLIGHTS]; + ModelInfo = m_pEntity->GetMatrix() * ModelInfo; + + firePos.x = ModelInfo.x; + firePos.y = ModelInfo.y; + firePos.z = ModelInfo.z + 0.15f; + } + + CParticle::AddParticle(PARTICLE_CARFLAME, firePos, + CVector(0.0f, 0.0f, CGeneral::GetRandomNumberInRange(0.0125f, 0.1f) * m_fStrength), + 0, m_fStrength, 0, 0, 0, 0); + + rand(); rand(); rand(); /* unsure why these three rands are called */ + + CParticle::AddParticle(PARTICLE_CARFLAME_SMOKE, firePos, + CVector(0.0f, 0.0f, 0.0f), 0, 0.0, 0, 0, 0, 0); + } + if (CTimer::m_snTimeInMilliseconds < m_nExtinguishTime || m_bIsScriptFire) { + if (CTimer::m_snTimeInMilliseconds > m_nStartTime) + m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + + nRandNumber = rand() & 127; + + lightpos.x = m_vecPos.x; + lightpos.y = m_vecPos.y; + lightpos.z = m_vecPos.z + 5.0f; + + if (!m_pEntity) { + CShadows::StoreStaticShadow((uint32)this, SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &lightpos, + 7.0, 0.0, 0.0, -7.0, 0, nRandNumber / 2, nRandNumber / 2, + 0, 10.0, 1.0, 40.0, 0, 0.0); + } + fGreen = nRandNumber / 128; + fRed = nRandNumber / 128; + + CPointLights::AddLight(0, m_vecPos, CVector(0.0f, 0.0f, 0.0f), + 12.0, fRed, fGreen, 0, 0, 0); + } else { + Extinguish(); + } +} + +void +CFire::ReportThisFire(void) +{ + gFireManager.m_nTotalFires++; + CEventList::RegisterEvent(EVENT_FIRE, m_vecPos, 1000); +} + +void +CFire::Extinguish(void) +{ + CPed *pCurrentEntity; + + if (m_bIsOngoing) { + if (!m_bIsScriptFire) + gFireManager.m_nTotalFires--; + + m_nExtinguishTime = 0; + m_bIsOngoing = false; + pCurrentEntity = (CPed *)m_pEntity; + + if (pCurrentEntity) { + if (pCurrentEntity->IsObject()) { + pCurrentEntity->RestorePreviousState(); + pCurrentEntity->m_pFire = 0; + } else if (pCurrentEntity->IsPed()) { + pCurrentEntity->m_vecOffsetSeek.x = 0.0; + } + m_pEntity = nil; + } + } +} + +void +CFireManager::StartFire(CVector pos, float size, bool propagation) +{ + CFire *pCurrentFire = GetNextFreeFire(); + + if (pCurrentFire) { + pCurrentFire->m_bIsOngoing = true; + pCurrentFire->m_bIsScriptFire = false; + pCurrentFire->m_bPropagationFlag = propagation; + pCurrentFire->m_bAudioSet = true; + pCurrentFire->m_vecPos = pos; + pCurrentFire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 10000; + pCurrentFire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + pCurrentFire->m_pEntity = nil; + pCurrentFire->m_pSource = nil; + pCurrentFire->field_24 = 0; + pCurrentFire->ReportThisFire(); + pCurrentFire->m_fStrength = size; + } +} + +void +CFireManager::StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength, bool propagation) +{ + CPed *ped = (CPed *)entityOnFire; + CVehicle *veh = (CVehicle *)entityOnFire; + CFire *fire = GetNextFreeFire(); + + if (!fire) + return; + if (entityOnFire->IsPed() && (ped->m_pFire || !ped->IsPedInControl())) { + return; + } else if (entityOnFire->IsVehicle() && veh->m_pCarFire || veh->IsCar() && ((CAutomobile *)veh)->Damage.GetEngineStatus() >= 225) { + return; + } + + if (entityOnFire->IsPed()) { + ped->m_pFire = fire; + if (ped != FindPlayerPed()) { + if (fleeFrom) { + ped->SetFlee(fleeFrom, 10000); + } else { + ped->SetFlee(ped->GetPosition(), 10000); + ped->m_fleeFrom = nil; + } + ped->bDrawLast = false; + ped->SetMoveState(PEDMOVE_SPRINT); + ped->SetMoveAnim(); + ped->m_nPedState = PED_ON_FIRE; + } + if (fleeFrom) { + if (ped->m_nPedType == PEDTYPE_COP) { + CEventList::RegisterEvent(EVENT_COP_SET_ON_FIRE, EVENT_ENTITY_PED, + entityOnFire, (CPed *)fleeFrom, 10000); + } else { + CEventList::RegisterEvent(EVENT_PED_SET_ON_FIRE, EVENT_ENTITY_PED, + entityOnFire, (CPed *)fleeFrom, 10000); + } + } + } else { + if (entityOnFire->IsVehicle()) { + veh->m_pCarFire = fire; + if (fleeFrom) { + CEventList::RegisterEvent(EVENT_CAR_SET_ON_FIRE, EVENT_ENTITY_VEHICLE, + entityOnFire, (CPed *)fleeFrom, 10000); + } + } + } + + fire->m_bIsOngoing = true; + fire->m_bIsScriptFire = false; + fire->m_vecPos = ped->GetPosition(); + + if (entityOnFire && entityOnFire->IsPed() && ped->IsPlayer()) { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 3333; + } else if (entityOnFire->IsVehicle()) { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + + 4000 + (signed int)((double)(unsigned __int16)rand() * 0.000030517578 * 1000.0f); //this needs to be simplified + } else { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + + 10000 + (signed int)((double)(unsigned __int16)rand() * 0.000030517578 * 1000.0f); + } + fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_pEntity = entityOnFire; + entityOnFire->RegisterReference(&fire->m_pEntity); + fire->m_pSource = fleeFrom; + if (fleeFrom) + fleeFrom->RegisterReference(&fire->m_pSource); + fire->ReportThisFire(); + fire->field_24 = 0; + fire->m_fStrength = strength; + fire->m_bPropagationFlag = propagation; + fire->m_bAudioSet = true; +} + +void +CFireManager::Update(void) +{ + for (int i = 0; i < NUM_FIRES; i++) { + if (m_aFires[i].m_bIsOngoing) + m_aFires[i].ProcessFire(); + } +} + +CFire * +CFireManager::FindNearestFire(CVector vecPos, float *pDistance) { for (int i = 0; i < MAX_FIREMEN_ATTENDING; i++) { int fireId = -1; @@ -38,6 +318,44 @@ CFire* CFireManager::FindNearestFire(CVector vecPos, float* pDistance) return nil; } +CFire * +CFireManager::FindFurthestFire_NeverMindFireMen(CVector coords, float minRange, float maxRange) +{ + int furthestFire = -1; + float lastFireDist = 0.0; + float fireDist; + + for (int i = 0; i < NUM_FIRES; i++) { + if (m_aFires[i].m_bIsOngoing && !m_aFires[i].m_bIsScriptFire) { + fireDist = (m_aFires[i].m_vecPos - coords).Magnitude2D(); + if (fireDist > minRange && fireDist < maxRange && fireDist > lastFireDist) { + lastFireDist = fireDist; + furthestFire = i; + } + } + } + if (furthestFire == -1) + return nil; + else + return &m_aFires[furthestFire]; +} + +CFire * +CFireManager::GetNextFreeFire(void) +{ + for (int i = 0; i < NUM_FIRES; i++) { + if (!m_aFires[i].m_bIsOngoing && !m_aFires[i].m_bIsScriptFire) + return &m_aFires[i]; + } + return nil; +} + +uint32 +CFireManager::GetTotalActiveFires(void) const +{ + return m_nTotalFires; +} + void CFireManager::ExtinguishPoint(CVector point, float range) { @@ -49,16 +367,102 @@ CFireManager::ExtinguishPoint(CVector point, float range) } } -WRAPPER void CFireManager::StartFire(CEntity *entityOnFire, CEntity *culprit, float, uint32) { EAXJMP(0x479590); } -WRAPPER void CFireManager::StartFire(CVector, float, uint8) { EAXJMP(0x479500); } -WRAPPER int32 CFireManager::StartScriptFire(const CVector& pos, CEntity* culprit, float, uint8) { EAXJMP(0x479E60); } -WRAPPER bool CFireManager::IsScriptFireExtinguish(int16) { EAXJMP(0x479FC0); } -WRAPPER void CFireManager::RemoveScriptFire(int16) { EAXJMP(0x479FE0); } -WRAPPER void CFireManager::RemoveAllScriptFires(void) { EAXJMP(0x47A000); } -WRAPPER void CFireManager::SetScriptFireAudio(int16, bool) { EAXJMP(0x47A040); } +int32 +CFireManager::StartScriptFire(const CVector &pos, CEntity *target, float strength, bool propagation) +{ + CFire *fire; + CPed *ped = (CPed *)target; + CVehicle *veh = (CVehicle *)target; + + if (target) { + if (target->IsPed()) { + if (ped->m_pFire) + ped->m_pFire->Extinguish(); + } else if (target->IsVehicle()) { + if (veh->m_pCarFire) + veh->m_pCarFire->Extinguish(); + if (veh->IsCar() && ((CAutomobile *)veh)->Damage.GetEngineStatus() >= 225) { + ((CAutomobile *)veh)->Damage.SetEngineStatus(215); + } + } + } + + fire = GetNextFreeFire(); + fire->m_bIsOngoing = true; + fire->m_bIsScriptFire = true; + fire->m_bPropagationFlag = propagation; + fire->m_bAudioSet = true; + fire->m_vecPos = pos; + fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_pEntity = target; + + if (target) + target->RegisterReference(&fire->m_pEntity); + fire->m_pSource = nil; + fire->field_24 = 0; + fire->m_fStrength = strength; + if (target) { + if (target->IsPed()) { + ped->m_pFire = fire; + if (target != (CVehicle *)FindPlayerPed()) { + CVector2D pos = target->GetPosition(); + ped->SetFlee(pos, 10000); + ped->SetMoveAnim(); + ped->m_nPedState = PED_ON_FIRE; + } + } else if (target->IsVehicle()) { + veh->m_pCarFire = fire; + } + } + return fire - m_aFires; +} + +bool +CFireManager::IsScriptFireExtinguish(int16 index) +{ + return (!m_aFires[index].m_bIsOngoing) ? true : false; +} + +void +CFireManager::RemoveAllScriptFires(void) +{ + for (int i = 0; i < NUM_FIRES; i++) { + if (m_aFires[i].m_bIsScriptFire) { + m_aFires[i].Extinguish(); + m_aFires[i].m_bIsScriptFire = false; + } + } +} + +void +CFireManager::RemoveScriptFire(int16 index) +{ + m_aFires[index].Extinguish(); + m_aFires[index].m_bIsScriptFire = false; +} + +void +CFireManager::SetScriptFireAudio(int16 index, bool state) +{ + m_aFires[index].m_bAudioSet = state; +} STARTPATCHES - InjectHook(0x479DB0, &CFireManager::ExtinguishPoint, PATCH_JUMP); + InjectHook(0x479220, &CFire_::ctor, PATCH_JUMP); + InjectHook(0x479280, &CFire_::dtor, PATCH_JUMP); + InjectHook(0x4798D0, &CFire::ProcessFire, PATCH_JUMP); + InjectHook(0x4798B0, &CFire::ReportThisFire, PATCH_JUMP); + InjectHook(0x479D40, &CFire::Extinguish, PATCH_JUMP); + InjectHook(0x479500, (void(CFireManager::*)(CVector pos, float size, bool propagation))&CFireManager::StartFire, PATCH_JUMP); + InjectHook(0x479590, (void(CFireManager::*)(CEntity *, CEntity *, float, bool))&CFireManager::StartFire, PATCH_JUMP); + InjectHook(0x479310, &CFireManager::Update, PATCH_JUMP); + InjectHook(0x479430, &CFireManager::FindFurthestFire_NeverMindFireMen, PATCH_JUMP); InjectHook(0x479340, &CFireManager::FindNearestFire, PATCH_JUMP); + InjectHook(0x4792E0, &CFireManager::GetNextFreeFire, PATCH_JUMP); + InjectHook(0x479DB0, &CFireManager::ExtinguishPoint, PATCH_JUMP); + InjectHook(0x479E60, &CFireManager::StartScriptFire, PATCH_JUMP); + InjectHook(0x479FC0, &CFireManager::IsScriptFireExtinguish, PATCH_JUMP); + InjectHook(0x47A000, &CFireManager::RemoveAllScriptFires, PATCH_JUMP); + InjectHook(0x479FE0, &CFireManager::RemoveScriptFire, PATCH_JUMP); + InjectHook(0x47A040, &CFireManager::SetScriptFireAudio, PATCH_JUMP); ENDPATCHES - From ea9e45fcda8468896ed156ae306bf50ad31acdaa Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:00:38 +0100 Subject: [PATCH 05/70] Update Fire.cpp --- src/core/Fire.cpp | 238 +++++++++++++++++++++++----------------------- 1 file changed, 117 insertions(+), 121 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index d53c8fc7..f46137f2 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -18,12 +18,12 @@ CFireManager &gFireManager = *(CFireManager*)0x8F31D0; -CFire::CFire(void) +CFire::CFire() { - m_bIsOngoing = 0; - m_bIsScriptFire = 0; - m_bPropagationFlag = 1; - m_bAudioSet = 1; + m_bIsOngoing = false; + m_bIsScriptFire = false; + m_bPropagationFlag = true; + m_bAudioSet = true; m_vecPos = CVector(0.0f, 0.0f, 0.0f); m_pEntity = 0; m_pSource = 0; @@ -35,7 +35,7 @@ CFire::CFire(void) m_fStrength = 0.8f; } -CFire::~CFire(void) {} +CFire::~CFire() {} class CFire_ : public CFire { public: @@ -49,16 +49,13 @@ CFire::ProcessFire(void) float fDamagePlayer; float fDamagePeds; float fDamageVehicle; - CVehicle *pCurrentVehicle; - char nRandNumber; + int8 nRandNumber; float fGreen; float fRed; CVector lightpos; CVector firePos; - CVector vecProduct; - - CEntity *pCurrentEntity = (CPed *)m_pEntity; - CPed *ped = (CPed *)pCurrentEntity; + CPed *ped = (CPed *)m_pEntity; + CVehicle *veh = (CVehicle*)m_pEntity; if (m_pEntity) { m_vecPos = m_pEntity->GetPosition(); @@ -70,35 +67,32 @@ CFire::ProcessFire(void) } if (ped->m_nMoveState != PEDMOVE_RUN) m_vecPos.z -= 1.0f; - if (ped->bInVehicle && ped->m_pMyVehicle) { if (ped->m_pMyVehicle->IsCar()) ped->m_pMyVehicle->m_fHealth = 75.0f; - } else if (pCurrentEntity == (CPed *)FindPlayerPed()) { + } else if (m_pEntity == (CPed *)FindPlayerPed()) { fDamagePlayer = 1.2f * CTimer::GetTimeStep(); - ((CPlayerPed *)pCurrentEntity)->InflictDamage( + ((CPlayerPed *)m_pEntity)->InflictDamage( (CPlayerPed *)m_pSource, WEAPONTYPE_FLAMETHROWER, fDamagePlayer, PEDPIECE_TORSO, 0); } else { fDamagePeds = 1.2f * CTimer::GetTimeStep(); - if (((CPlayerPed *)pCurrentEntity)->InflictDamage( + if (((CPlayerPed *)m_pEntity)->InflictDamage( (CPlayerPed *)m_pSource, WEAPONTYPE_FLAMETHROWER, - fDamagePeds, PEDPIECE_TORSO, 0)) - - pCurrentEntity->bRenderScorched = true; + fDamagePeds, PEDPIECE_TORSO, 0)) { + m_pEntity->bRenderScorched = true; + } } - } else if (pCurrentEntity->IsVehicle()) { - CVehicle *pCurrentVehicle = (CVehicle*)m_pEntity; - - if (pCurrentVehicle->m_pCarFire != this) { + } else if (m_pEntity->IsVehicle()) { + if (veh->m_pCarFire != this) { Extinguish(); return; } if (!m_bIsScriptFire) { fDamageVehicle = 1.2f * CTimer::GetTimeStep(); - pCurrentVehicle->InflictDamage((CVehicle *)m_pSource, WEAPONTYPE_FLAMETHROWER, fDamageVehicle); + veh->InflictDamage((CVehicle *)m_pSource, WEAPONTYPE_FLAMETHROWER, fDamageVehicle); } } } @@ -110,10 +104,9 @@ CFire::ProcessFire(void) if (CTimer::m_snTimeInMilliseconds > field_24) { /* set to 0 when a newfire starts, related to time */ field_24 = CTimer::m_snTimeInMilliseconds + 80; firePos = m_vecPos; - pCurrentVehicle = (CVehicle *)m_pEntity; - if (pCurrentVehicle && pCurrentVehicle->IsVehicle() && (pCurrentVehicle->IsCar())) { - CVehicleModelInfo *mi = ((CVehicleModelInfo*)CModelInfo::GetModelInfo(pCurrentVehicle->GetModelIndex())); + if (veh && veh->IsVehicle() && veh->IsCar()) { + CVehicleModelInfo *mi = ((CVehicleModelInfo*)CModelInfo::GetModelInfo(veh->GetModelIndex())); CVector ModelInfo = mi->m_positions[CAR_POS_HEADLIGHTS]; ModelInfo = m_pEntity->GetMatrix() * ModelInfo; @@ -124,7 +117,7 @@ CFire::ProcessFire(void) CParticle::AddParticle(PARTICLE_CARFLAME, firePos, CVector(0.0f, 0.0f, CGeneral::GetRandomNumberInRange(0.0125f, 0.1f) * m_fStrength), - 0, m_fStrength, 0, 0, 0, 0); + 0, m_fStrength, 0, 0, 0, 0); rand(); rand(); rand(); /* unsure why these three rands are called */ @@ -135,8 +128,7 @@ CFire::ProcessFire(void) if (CTimer::m_snTimeInMilliseconds > m_nStartTime) m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; - nRandNumber = rand() & 127; - + nRandNumber = CGeneral::GetRandomNumber(); lightpos.x = m_vecPos.x; lightpos.y = m_vecPos.y; lightpos.z = m_vecPos.z + 5.0f; @@ -166,22 +158,19 @@ CFire::ReportThisFire(void) void CFire::Extinguish(void) { - CPed *pCurrentEntity; - if (m_bIsOngoing) { if (!m_bIsScriptFire) gFireManager.m_nTotalFires--; m_nExtinguishTime = 0; m_bIsOngoing = false; - pCurrentEntity = (CPed *)m_pEntity; - if (pCurrentEntity) { - if (pCurrentEntity->IsObject()) { - pCurrentEntity->RestorePreviousState(); - pCurrentEntity->m_pFire = 0; - } else if (pCurrentEntity->IsPed()) { - pCurrentEntity->m_vecOffsetSeek.x = 0.0; + if (m_pEntity) { + if (m_pEntity->IsPed()) { + ((CPed *)m_pEntity)->RestorePreviousState(); + ((CPed *)m_pEntity)->m_pFire = 0; + } else if (m_pEntity->IsVehicle()) { + ((CVehicle *)m_pEntity)->m_pCarFire = nil; } m_pEntity = nil; } @@ -191,96 +180,104 @@ CFire::Extinguish(void) void CFireManager::StartFire(CVector pos, float size, bool propagation) { - CFire *pCurrentFire = GetNextFreeFire(); + CFire *fire = GetNextFreeFire(); - if (pCurrentFire) { - pCurrentFire->m_bIsOngoing = true; - pCurrentFire->m_bIsScriptFire = false; - pCurrentFire->m_bPropagationFlag = propagation; - pCurrentFire->m_bAudioSet = true; - pCurrentFire->m_vecPos = pos; - pCurrentFire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 10000; - pCurrentFire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; - pCurrentFire->m_pEntity = nil; - pCurrentFire->m_pSource = nil; - pCurrentFire->field_24 = 0; - pCurrentFire->ReportThisFire(); - pCurrentFire->m_fStrength = size; + if (fire) { + fire->m_bIsOngoing = true; + fire->m_bIsScriptFire = false; + fire->m_bPropagationFlag = propagation; + fire->m_bAudioSet = true; + fire->m_vecPos = pos; + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 10000; + fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_pEntity = nil; + fire->m_pSource = nil; + fire->field_24 = 0; + fire->ReportThisFire(); + fire->m_fStrength = size; } } -void +CFire * CFireManager::StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength, bool propagation) { CPed *ped = (CPed *)entityOnFire; CVehicle *veh = (CVehicle *)entityOnFire; - CFire *fire = GetNextFreeFire(); - - if (!fire) - return; - if (entityOnFire->IsPed() && (ped->m_pFire || !ped->IsPedInControl())) { - return; - } else if (entityOnFire->IsVehicle() && veh->m_pCarFire || veh->IsCar() && ((CAutomobile *)veh)->Damage.GetEngineStatus() >= 225) { - return; - } - + if (entityOnFire->IsPed()) { - ped->m_pFire = fire; - if (ped != FindPlayerPed()) { - if (fleeFrom) { - ped->SetFlee(fleeFrom, 10000); - } else { - ped->SetFlee(ped->GetPosition(), 10000); - ped->m_fleeFrom = nil; - } - ped->bDrawLast = false; - ped->SetMoveState(PEDMOVE_SPRINT); - ped->SetMoveAnim(); - ped->m_nPedState = PED_ON_FIRE; - } - if (fleeFrom) { - if (ped->m_nPedType == PEDTYPE_COP) { - CEventList::RegisterEvent(EVENT_COP_SET_ON_FIRE, EVENT_ENTITY_PED, - entityOnFire, (CPed *)fleeFrom, 10000); - } else { - CEventList::RegisterEvent(EVENT_PED_SET_ON_FIRE, EVENT_ENTITY_PED, - entityOnFire, (CPed *)fleeFrom, 10000); - } - } - } else { - if (entityOnFire->IsVehicle()) { - veh->m_pCarFire = fire; - if (fleeFrom) { - CEventList::RegisterEvent(EVENT_CAR_SET_ON_FIRE, EVENT_ENTITY_VEHICLE, - entityOnFire, (CPed *)fleeFrom, 10000); - } - } - } - - fire->m_bIsOngoing = true; - fire->m_bIsScriptFire = false; - fire->m_vecPos = ped->GetPosition(); - - if (entityOnFire && entityOnFire->IsPed() && ped->IsPlayer()) { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 3333; + if (ped->m_pFire) + return nil; + if (!ped->IsPedInControl()) + return nil; } else if (entityOnFire->IsVehicle()) { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds - + 4000 + (signed int)((double)(unsigned __int16)rand() * 0.000030517578 * 1000.0f); //this needs to be simplified - } else { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds - + 10000 + (signed int)((double)(unsigned __int16)rand() * 0.000030517578 * 1000.0f); + if (veh->m_pCarFire) + return nil; + if (veh->IsCar() && ((CAutomobile *)veh)->Damage.GetEngineStatus() >= 225) + return nil; } - fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; - fire->m_pEntity = entityOnFire; - entityOnFire->RegisterReference(&fire->m_pEntity); - fire->m_pSource = fleeFrom; - if (fleeFrom) - fleeFrom->RegisterReference(&fire->m_pSource); - fire->ReportThisFire(); - fire->field_24 = 0; - fire->m_fStrength = strength; - fire->m_bPropagationFlag = propagation; - fire->m_bAudioSet = true; + CFire *fire = GetNextFreeFire(); + + if (fire) { + if (entityOnFire->IsPed()) { + ped->m_pFire = fire; + if (ped != FindPlayerPed()) { + if (fleeFrom) { + ped->SetFlee(fleeFrom, 10000); + } else { + CVector2D pos = entityOnFire->GetPosition(); + ped->SetFlee(pos, 10000); + ped->m_fleeFrom = nil; + } + ped->bDrawLast = false; + ped->SetMoveState(PEDMOVE_SPRINT); + ped->SetMoveAnim(); + ped->m_nPedState = PED_ON_FIRE; + } + if (fleeFrom) { + if (ped->m_nPedType == PEDTYPE_COP) { + CEventList::RegisterEvent(EVENT_COP_SET_ON_FIRE, EVENT_ENTITY_PED, + entityOnFire, (CPed *)fleeFrom, 10000); + } else { + CEventList::RegisterEvent(EVENT_PED_SET_ON_FIRE, EVENT_ENTITY_PED, + entityOnFire, (CPed *)fleeFrom, 10000); + } + } + } else { + if (entityOnFire->IsVehicle()) { + veh->m_pCarFire = fire; + if (fleeFrom) { + CEventList::RegisterEvent(EVENT_CAR_SET_ON_FIRE, EVENT_ENTITY_VEHICLE, + entityOnFire, (CPed *)fleeFrom, 10000); + } + } + } + + fire->m_bIsOngoing = true; + fire->m_bIsScriptFire = false; + fire->m_vecPos = entityOnFire->GetPosition(); + + if (entityOnFire && entityOnFire->IsPed() && ped->IsPlayer()) { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 3333; + } else if (entityOnFire->IsVehicle()) { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + CGeneral::GetRandomNumberInRange(4000, 5000); + } else { + fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + CGeneral::GetRandomNumberInRange(10000, 11000); + } + fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_pEntity = entityOnFire; + + entityOnFire->RegisterReference(&fire->m_pEntity); + fire->m_pSource = fleeFrom; + + if (fleeFrom) + fleeFrom->RegisterReference(&fire->m_pSource); + fire->ReportThisFire(); + fire->field_24 = 0; + fire->m_fStrength = strength; + fire->m_bPropagationFlag = propagation; + fire->m_bAudioSet = true; + } + return fire; } void @@ -292,8 +289,7 @@ CFireManager::Update(void) } } -CFire * -CFireManager::FindNearestFire(CVector vecPos, float *pDistance) +CFire* CFireManager::FindNearestFire(CVector vecPos, float *pDistance) { for (int i = 0; i < MAX_FIREMEN_ATTENDING; i++) { int fireId = -1; @@ -454,7 +450,7 @@ STARTPATCHES InjectHook(0x4798B0, &CFire::ReportThisFire, PATCH_JUMP); InjectHook(0x479D40, &CFire::Extinguish, PATCH_JUMP); InjectHook(0x479500, (void(CFireManager::*)(CVector pos, float size, bool propagation))&CFireManager::StartFire, PATCH_JUMP); - InjectHook(0x479590, (void(CFireManager::*)(CEntity *, CEntity *, float, bool))&CFireManager::StartFire, PATCH_JUMP); + InjectHook(0x479590, (CFire *(CFireManager::*)(CEntity *, CEntity *, float, bool))&CFireManager::StartFire, PATCH_JUMP); InjectHook(0x479310, &CFireManager::Update, PATCH_JUMP); InjectHook(0x479430, &CFireManager::FindFurthestFire_NeverMindFireMen, PATCH_JUMP); InjectHook(0x479340, &CFireManager::FindNearestFire, PATCH_JUMP); From 834f4f6d4c423683bf78e388418a260094c7bf21 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:01:10 +0100 Subject: [PATCH 06/70] Update Fire.h --- src/core/Fire.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/Fire.h b/src/core/Fire.h index 89ab9a9f..aa65fb8f 100644 --- a/src/core/Fire.h +++ b/src/core/Fire.h @@ -14,7 +14,7 @@ public: CEntity *m_pSource; uint32 m_nExtinguishTime; uint32 m_nStartTime; - int32 field_20; + int field_20; uint32 field_24; uint32 m_nFiremenPuttingOut; float m_fStrength; @@ -35,12 +35,12 @@ public: uint32 m_nTotalFires; CFire m_aFires[NUM_FIRES]; void StartFire(CVector pos, float size, bool propagation); - void StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength, bool propagation); + CFire *StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength, bool propagation); void Update(void); CFire *FindFurthestFire_NeverMindFireMen(CVector coords, float minRange, float maxRange); CFire *FindNearestFire(CVector vecPos, float *pDistance); CFire *GetNextFreeFire(void); - uint32 GetTotalActiveFires(void) const; + uint32 GetTotalActiveFires() const; void ExtinguishPoint(CVector point, float range); int32 StartScriptFire(const CVector &pos, CEntity *target, float strength, bool propagation); bool IsScriptFireExtinguish(int16 index); From 0605e704ca63d1cf57cb0f5e2c9d2dc0b18e0249 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:01:38 +0100 Subject: [PATCH 07/70] Update Fire.h --- src/core/Fire.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Fire.h b/src/core/Fire.h index aa65fb8f..e6c7dd7d 100644 --- a/src/core/Fire.h +++ b/src/core/Fire.h @@ -14,7 +14,7 @@ public: CEntity *m_pSource; uint32 m_nExtinguishTime; uint32 m_nStartTime; - int field_20; + int32 field_20; uint32 field_24; uint32 m_nFiremenPuttingOut; float m_fStrength; From e22ce9404c146daf90e5f32921cf9b043101e56a Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:07:07 +0100 Subject: [PATCH 08/70] Update Fire.cpp --- src/core/Fire.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index f46137f2..95856a0f 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -37,12 +37,6 @@ CFire::CFire() CFire::~CFire() {} -class CFire_ : public CFire { -public: - void ctor() { ::new(this) CFire(); } - void dtor() { CFire::CFire(); } -}; - void CFire::ProcessFire(void) { @@ -444,8 +438,6 @@ CFireManager::SetScriptFireAudio(int16 index, bool state) } STARTPATCHES - InjectHook(0x479220, &CFire_::ctor, PATCH_JUMP); - InjectHook(0x479280, &CFire_::dtor, PATCH_JUMP); InjectHook(0x4798D0, &CFire::ProcessFire, PATCH_JUMP); InjectHook(0x4798B0, &CFire::ReportThisFire, PATCH_JUMP); InjectHook(0x479D40, &CFire::Extinguish, PATCH_JUMP); From 08e1c869a4b8a91859f46d739f1d9a802500c677 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:09:27 +0100 Subject: [PATCH 09/70] renamed field_24 to m_nNextTimeToAddFlames --- src/core/Fire.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Fire.h b/src/core/Fire.h index e6c7dd7d..a4599d11 100644 --- a/src/core/Fire.h +++ b/src/core/Fire.h @@ -15,7 +15,7 @@ public: uint32 m_nExtinguishTime; uint32 m_nStartTime; int32 field_20; - uint32 field_24; + uint32 m_nNextTimeToAddFlames; uint32 m_nFiremenPuttingOut; float m_fStrength; From 96802f9b95219bc7ea9869d0ec1178ff6d4193c3 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:13:06 +0100 Subject: [PATCH 10/70] Update Fire.cpp --- src/core/Fire.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index 95856a0f..e6756884 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -31,7 +31,7 @@ CFire::CFire() m_nExtinguishTime = 0; m_nStartTime = 0; field_20 = 1; - field_24 = 0; + m_nNextTimeToAddFlames = 0; m_fStrength = 0.8f; } @@ -95,8 +95,8 @@ CFire::ProcessFire(void) FindPlayerPed()->DoStuffToGoOnFire(); gFireManager.StartFire(FindPlayerPed(), m_pSource, 0.8f, 1); } - if (CTimer::m_snTimeInMilliseconds > field_24) { /* set to 0 when a newfire starts, related to time */ - field_24 = CTimer::m_snTimeInMilliseconds + 80; + if (CTimer::m_snTimeInMilliseconds > m_nNextTimeToAddFlames) { + m_nNextTimeToAddFlames = CTimer::m_snTimeInMilliseconds + 80; firePos = m_vecPos; if (veh && veh->IsVehicle() && veh->IsCar()) { @@ -122,7 +122,7 @@ CFire::ProcessFire(void) if (CTimer::m_snTimeInMilliseconds > m_nStartTime) m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; - nRandNumber = CGeneral::GetRandomNumber(); + nRandNumber = CGeneral::GetRandomNumber() & 127; lightpos.x = m_vecPos.x; lightpos.y = m_vecPos.y; lightpos.z = m_vecPos.z + 5.0f; @@ -162,7 +162,7 @@ CFire::Extinguish(void) if (m_pEntity) { if (m_pEntity->IsPed()) { ((CPed *)m_pEntity)->RestorePreviousState(); - ((CPed *)m_pEntity)->m_pFire = 0; + ((CPed *)m_pEntity)->m_pFire = nil; } else if (m_pEntity->IsVehicle()) { ((CVehicle *)m_pEntity)->m_pCarFire = nil; } @@ -186,7 +186,7 @@ CFireManager::StartFire(CVector pos, float size, bool propagation) fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; fire->m_pEntity = nil; fire->m_pSource = nil; - fire->field_24 = 0; + fire->m_nNextTimeToAddFlames = 0; fire->ReportThisFire(); fire->m_fStrength = size; } From c3841fe5b48f3b1c567d03273a2ea23bacac89ca Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 21:24:32 +0100 Subject: [PATCH 11/70] m_snTimeInMilliseconds to GetTimeInMilliseconds() --- src/core/Fire.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index e6756884..05c4fe96 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -95,8 +95,8 @@ CFire::ProcessFire(void) FindPlayerPed()->DoStuffToGoOnFire(); gFireManager.StartFire(FindPlayerPed(), m_pSource, 0.8f, 1); } - if (CTimer::m_snTimeInMilliseconds > m_nNextTimeToAddFlames) { - m_nNextTimeToAddFlames = CTimer::m_snTimeInMilliseconds + 80; + if (CTimer::GetTimeInMilliseconds() > m_nNextTimeToAddFlames) { + m_nNextTimeToAddFlames = CTimer::GetTimeInMilliseconds() + 80; firePos = m_vecPos; if (veh && veh->IsVehicle() && veh->IsCar()) { @@ -118,9 +118,9 @@ CFire::ProcessFire(void) CParticle::AddParticle(PARTICLE_CARFLAME_SMOKE, firePos, CVector(0.0f, 0.0f, 0.0f), 0, 0.0, 0, 0, 0, 0); } - if (CTimer::m_snTimeInMilliseconds < m_nExtinguishTime || m_bIsScriptFire) { - if (CTimer::m_snTimeInMilliseconds > m_nStartTime) - m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + if (CTimer::GetTimeInMilliseconds() < m_nExtinguishTime || m_bIsScriptFire) { + if (CTimer::GetTimeInMilliseconds() > m_nStartTime) + m_nStartTime = CTimer::GetTimeInMilliseconds() + 400; nRandNumber = CGeneral::GetRandomNumber() & 127; lightpos.x = m_vecPos.x; @@ -182,8 +182,8 @@ CFireManager::StartFire(CVector pos, float size, bool propagation) fire->m_bPropagationFlag = propagation; fire->m_bAudioSet = true; fire->m_vecPos = pos; - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 10000; - fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_nExtinguishTime = CTimer::GetTimeInMilliseconds() + 10000; + fire->m_nStartTime = CTimer::GetTimeInMilliseconds() + 400; fire->m_pEntity = nil; fire->m_pSource = nil; fire->m_nNextTimeToAddFlames = 0; @@ -251,13 +251,13 @@ CFireManager::StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength fire->m_vecPos = entityOnFire->GetPosition(); if (entityOnFire && entityOnFire->IsPed() && ped->IsPlayer()) { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + 3333; + fire->m_nExtinguishTime = CTimer::GetTimeInMilliseconds() + 3333; } else if (entityOnFire->IsVehicle()) { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + CGeneral::GetRandomNumberInRange(4000, 5000); + fire->m_nExtinguishTime = CTimer::GetTimeInMilliseconds() + CGeneral::GetRandomNumberInRange(4000, 5000); } else { - fire->m_nExtinguishTime = CTimer::m_snTimeInMilliseconds + CGeneral::GetRandomNumberInRange(10000, 11000); + fire->m_nExtinguishTime = CTimer::GetTimeInMilliseconds() + CGeneral::GetRandomNumberInRange(10000, 11000); } - fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_nStartTime = CTimer::GetTimeInMilliseconds() + 400; fire->m_pEntity = entityOnFire; entityOnFire->RegisterReference(&fire->m_pEntity); @@ -383,7 +383,7 @@ CFireManager::StartScriptFire(const CVector &pos, CEntity *target, float strengt fire->m_bPropagationFlag = propagation; fire->m_bAudioSet = true; fire->m_vecPos = pos; - fire->m_nStartTime = CTimer::m_snTimeInMilliseconds + 400; + fire->m_nStartTime = CTimer::GetTimeInMilliseconds() + 400; fire->m_pEntity = target; if (target) From 09af0bcaf8a8b9648a95a89bf2fbab3c1f6b54c8 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 22:54:14 +0100 Subject: [PATCH 12/70] Update Fire.cpp --- src/core/Fire.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index 05c4fe96..13d53d03 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -266,7 +266,7 @@ CFireManager::StartFire(CEntity *entityOnFire, CEntity *fleeFrom, float strength if (fleeFrom) fleeFrom->RegisterReference(&fire->m_pSource); fire->ReportThisFire(); - fire->field_24 = 0; + fire->m_nNextTimeToAddFlames = 0; fire->m_fStrength = strength; fire->m_bPropagationFlag = propagation; fire->m_bAudioSet = true; @@ -389,7 +389,7 @@ CFireManager::StartScriptFire(const CVector &pos, CEntity *target, float strengt if (target) target->RegisterReference(&fire->m_pEntity); fire->m_pSource = nil; - fire->field_24 = 0; + fire->m_nNextTimeToAddFlames = 0; fire->m_fStrength = strength; if (target) { if (target->IsPed()) { From 93a4bc31fa767df0460d1e66e97f30c863cdc1bf Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 22:57:34 +0100 Subject: [PATCH 13/70] added wrapper for InflictDamage() --- src/vehicles/Vehicle.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vehicles/Vehicle.cpp b/src/vehicles/Vehicle.cpp index 90848d6c..6aa12841 100644 --- a/src/vehicles/Vehicle.cpp +++ b/src/vehicles/Vehicle.cpp @@ -32,6 +32,7 @@ void CVehicle::operator delete(void *p, int handle) { CPools::GetVehiclePool()-> WRAPPER bool CVehicle::ShufflePassengersToMakeSpace(void) { EAXJMP(0x5528A0); } // or Weapon.cpp? WRAPPER void FireOneInstantHitRound(CVector *shotSource, CVector *shotTarget, int32 damage) { EAXJMP(0x563B00); } +WRAPPER void CVehicle::InflictDamage(CVehicle *damagedBy, uint32 weaponType, float damage) { EAXJMP(0x551950); } CVehicle::CVehicle(uint8 CreatedBy) { From be2aca47cfa84626ded8c140e5f8cf4af5f1a055 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 22:58:20 +0100 Subject: [PATCH 14/70] added InflictDamage() declaration --- src/vehicles/Vehicle.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vehicles/Vehicle.h b/src/vehicles/Vehicle.h index bd8df694..68b7b2ca 100644 --- a/src/vehicles/Vehicle.h +++ b/src/vehicles/Vehicle.h @@ -266,6 +266,7 @@ public: void ProcessCarAlarm(void); bool IsSphereTouchingVehicle(float sx, float sy, float sz, float radius); bool ShufflePassengersToMakeSpace(void); + void InflictDamage(CEntity *damagedBy, uint32 weaponType, float damage); bool IsAlarmOn(void) { return m_nAlarmState != 0 && m_nAlarmState != -1; } CVehicleModelInfo* GetModelInfo() { return (CVehicleModelInfo*)CModelInfo::GetModelInfo(GetModelIndex()); } From a66db9efcb5d3245248ac4b5b9accfe8e6b99768 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Wed, 25 Mar 2020 23:01:22 +0100 Subject: [PATCH 15/70] Update Fire.cpp --- src/core/Fire.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index 13d53d03..864c2509 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -25,8 +25,8 @@ CFire::CFire() m_bPropagationFlag = true; m_bAudioSet = true; m_vecPos = CVector(0.0f, 0.0f, 0.0f); - m_pEntity = 0; - m_pSource = 0; + m_pEntity = nil; + m_pSource = nil; m_nFiremenPuttingOut = 0; m_nExtinguishTime = 0; m_nStartTime = 0; From 0e9101594031fb9468d639ab6e4b57449cd8997c Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Thu, 26 Mar 2020 14:19:55 +0100 Subject: [PATCH 16/70] Update Vehicle.cpp --- src/vehicles/Vehicle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vehicles/Vehicle.cpp b/src/vehicles/Vehicle.cpp index 6aa12841..ac0da52c 100644 --- a/src/vehicles/Vehicle.cpp +++ b/src/vehicles/Vehicle.cpp @@ -32,7 +32,7 @@ void CVehicle::operator delete(void *p, int handle) { CPools::GetVehiclePool()-> WRAPPER bool CVehicle::ShufflePassengersToMakeSpace(void) { EAXJMP(0x5528A0); } // or Weapon.cpp? WRAPPER void FireOneInstantHitRound(CVector *shotSource, CVector *shotTarget, int32 damage) { EAXJMP(0x563B00); } -WRAPPER void CVehicle::InflictDamage(CVehicle *damagedBy, uint32 weaponType, float damage) { EAXJMP(0x551950); } +WRAPPER void CVehicle::InflictDamage(CEntity *damagedBy, uint32 weaponType, float damage) { EAXJMP(0x551950); } CVehicle::CVehicle(uint8 CreatedBy) { From 0fe55eb5432906016cc3526caf3f86d5bf85aff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Wed, 25 Mar 2020 17:13:06 +0300 Subject: [PATCH 17/70] CCopPed 2 and fixes --- src/control/PathFind.cpp | 17 +- src/control/PathFind.h | 9 + src/control/RoadBlocks.cpp | 34 +++- src/control/RoadBlocks.h | 4 + src/core/ControllerConfig.cpp | 10 ++ src/core/EventList.cpp | 23 ++- src/core/EventList.h | 2 +- src/core/Frontend.cpp | 268 ++++++++++++++++------------- src/core/Frontend.h | 12 +- src/core/Timer.h | 13 +- src/core/config.h | 7 +- src/peds/CopPed.cpp | 287 +++++++++++++++++++++++++++++++- src/peds/CopPed.h | 5 +- src/peds/Ped.cpp | 8 + src/peds/Population.cpp | 2 +- src/save/GenericGameStorage.cpp | 45 +++-- src/save/PCSave.cpp | 14 +- 17 files changed, 575 insertions(+), 185 deletions(-) diff --git a/src/control/PathFind.cpp b/src/control/PathFind.cpp index daa27e57..608a209a 100644 --- a/src/control/PathFind.cpp +++ b/src/control/PathFind.cpp @@ -11,19 +11,10 @@ CPathFind &ThePaths = *(CPathFind*)0x8F6754; WRAPPER bool CPedPath::CalcPedRoute(uint8, CVector, CVector, CVector*, int16*, int16) { EAXJMP(0x42E680); } -enum -{ - NodeTypeExtern = 1, - NodeTypeIntern = 2, - - ObjectFlag1 = 1, - ObjectEastWest = 2, - - MAX_DIST = INT16_MAX-1 -}; +#define MAX_DIST INT16_MAX-1 // object flags: -// 1 +// 1 UseInRoadBlock // 2 east/west road(?) CPathInfoForObject *&InfoForTileCars = *(CPathInfoForObject**)0x8F1A8C; @@ -218,14 +209,14 @@ CPathFind::PreparePathData(void) if(numIntern == 1 && numExtern == 2){ if(numLanes < 4){ if((i & 7) == 4){ // WHAT? - m_objectFlags[i] |= ObjectFlag1; + m_objectFlags[i] |= UseInRoadBlock; if(maxX > maxY) m_objectFlags[i] |= ObjectEastWest; else m_objectFlags[i] &= ~ObjectEastWest; } }else{ - m_objectFlags[i] |= ObjectFlag1; + m_objectFlags[i] |= UseInRoadBlock; if(maxX > maxY) m_objectFlags[i] |= ObjectEastWest; else diff --git a/src/control/PathFind.h b/src/control/PathFind.h index d42b8bb3..c51cb7c7 100644 --- a/src/control/PathFind.h +++ b/src/control/PathFind.h @@ -9,6 +9,15 @@ public: static bool CalcPedRoute(uint8, CVector, CVector, CVector*, int16*, int16); }; +enum +{ + NodeTypeExtern = 1, + NodeTypeIntern = 2, + + UseInRoadBlock = 1, + ObjectEastWest = 2, +}; + enum { PATH_CAR = 0, diff --git a/src/control/RoadBlocks.cpp b/src/control/RoadBlocks.cpp index ed092391..e39fe481 100644 --- a/src/control/RoadBlocks.cpp +++ b/src/control/RoadBlocks.cpp @@ -1,7 +1,37 @@ #include "common.h" #include "patcher.h" #include "RoadBlocks.h" +#include "PathFind.h" + +int16 &CRoadBlocks::NumRoadBlocks = *(int16*)0x95CC34; +int16 (&CRoadBlocks::RoadBlockObjects)[NUMROADBLOCKS] = *(int16(*)[NUMROADBLOCKS]) * (uintptr*)0x72B3A8; +bool (&CRoadBlocks::InOrOut)[NUMROADBLOCKS] = *(bool(*)[NUMROADBLOCKS]) * (uintptr*)0x733810; -WRAPPER void CRoadBlocks::Init(void) { EAXJMP(0x436F50); } WRAPPER void CRoadBlocks::GenerateRoadBlockCopsForCar(CVehicle*, int32, int16) { EAXJMP(0x4376A0); } -WRAPPER void CRoadBlocks::GenerateRoadBlocks(void) { EAXJMP(0x436FA0); } \ No newline at end of file +WRAPPER void CRoadBlocks::GenerateRoadBlocks(void) { EAXJMP(0x436FA0); } + +void +CRoadBlocks::Init(void) +{ + NumRoadBlocks = 0; + for (int objId = 0; objId < ThePaths.m_numMapObjects; objId++) { + if (ThePaths.m_objectFlags[objId] & UseInRoadBlock) { + if (NumRoadBlocks < 600) { + InOrOut[NumRoadBlocks] = true; + RoadBlockObjects[NumRoadBlocks] = objId; + NumRoadBlocks++; + } else { +#ifndef MASTER + printf("Not enough room for the potential roadblocks\n"); +#endif + // FIX: Don't iterate loop after NUMROADBLOCKS + return; + } + } + } + +} + +STARTPATCHES + InjectHook(0x436F50, &CRoadBlocks::Init, PATCH_JUMP); +ENDPATCHES \ No newline at end of file diff --git a/src/control/RoadBlocks.h b/src/control/RoadBlocks.h index b1bb3589..3f5868e7 100644 --- a/src/control/RoadBlocks.h +++ b/src/control/RoadBlocks.h @@ -6,6 +6,10 @@ class CVehicle; class CRoadBlocks { public: + static int16 (&NumRoadBlocks); + static int16 (&RoadBlockObjects)[NUMROADBLOCKS]; + static bool (&InOrOut)[NUMROADBLOCKS]; + static void Init(void); static void GenerateRoadBlockCopsForCar(CVehicle*, int32, int16); static void GenerateRoadBlocks(void); diff --git a/src/core/ControllerConfig.cpp b/src/core/ControllerConfig.cpp index 92c51182..541257c6 100644 --- a/src/core/ControllerConfig.cpp +++ b/src/core/ControllerConfig.cpp @@ -417,6 +417,11 @@ void CControllerConfigManager::UpdateJoyInConfigMenus_ButtonDown(int32 button, i case 13: pad->PCTempJoyState.DPadUp = 255; break; +#ifdef REGISTER_START_BUTTON + case 12: + pad->PCTempJoyState.Start = 255; + break; +#endif case 11: pad->PCTempJoyState.RightShock = 255; break; @@ -839,6 +844,11 @@ void CControllerConfigManager::UpdateJoyInConfigMenus_ButtonUp(int32 button, int case 13: pad->PCTempJoyState.DPadUp = 0; break; +#ifdef REGISTER_START_BUTTON + case 12: + pad->PCTempJoyState.Start = 0; + break; +#endif case 11: pad->PCTempJoyState.RightShock = 0; break; diff --git a/src/core/EventList.cpp b/src/core/EventList.cpp index caf0cb3f..4364359a 100644 --- a/src/core/EventList.cpp +++ b/src/core/EventList.cpp @@ -5,10 +5,13 @@ #include "World.h" #include "Wanted.h" #include "EventList.h" +#include "Messages.h" +#include "Text.h" +#include "main.h" int32 CEventList::ms_nFirstFreeSlotIndex; -//CEvent gaEvent[NUMEVENTS]; -CEvent *gaEvent = (CEvent*)0x6EF830; +CEvent gaEvent[NUMEVENTS]; +//CEvent *gaEvent = (CEvent*)0x6EF830; enum { @@ -207,8 +210,20 @@ CEventList::ReportCrimeForEvent(eEventType type, int32 crimeId, bool copsDontCar default: crime = CRIME_NONE; break; } - if(crime == CRIME_NONE) - return; +#ifdef VC_PED_PORTS + if (crime == CRIME_HIT_PED && ((CPed*)crimeId)->IsPointerValid() && + FindPlayerPed()->m_pWanted->m_nWantedLevel == 0 && ((CPed*)crimeId)->m_ped_flagE2) { + + if(!((CPed*)crimeId)->DyingOrDead()) { + sprintf(gString, "$50 Good Citizen Bonus!"); + AsciiToUnicode(gString, gUString); + CMessages::AddBigMessage(gUString, 5000, 0); + CWorld::Players[CWorld::PlayerInFocus].m_nMoney += 50; + } + } else +#endif + if(crime == CRIME_NONE) + return; CVector playerPedCoors = FindPlayerPed()->GetPosition(); CVector playerCoors = FindPlayerCoors(); diff --git a/src/core/EventList.h b/src/core/EventList.h index 2799fca4..1c03c9d6 100644 --- a/src/core/EventList.h +++ b/src/core/EventList.h @@ -63,4 +63,4 @@ public: static void ReportCrimeForEvent(eEventType type, int32, bool); }; -extern CEvent *gaEvent; \ No newline at end of file +extern CEvent gaEvent[NUMEVENTS]; \ No newline at end of file diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 4c2f3afa..aff8a3ec 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -181,6 +181,7 @@ ScaleAndCenterX(float x) #endif #define isPlainTextScreen(screen) (screen == MENUPAGE_BRIEFS || screen == MENUPAGE_STATS) + #ifdef PS2_LIKE_MENU #define ChangeScreen(screen, option, updateDelay, withReverseAlpha) \ do { \ @@ -235,67 +236,100 @@ ScaleAndCenterX(float x) m_nHoverOption = HOVEROPTION_NOT_HOVERING; \ } while(0) -#define ScrollUpListByOne() \ - do { \ - if (m_nSelectedListRow == m_nFirstVisibleRowOnList) { \ - if (m_nFirstVisibleRowOnList > 0) { \ - m_nSelectedListRow--; \ - m_nFirstVisibleRowOnList--; \ - m_nCurListItemY -= LIST_HEIGHT / m_nTotalListRow; \ - } \ - } else { \ - m_nSelectedListRow--; \ - } \ - } while(0) +// --- Functions not in the game/inlined starts -#define ScrollDownListByOne() \ - do { \ - if (m_nSelectedListRow == m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW - 1) { \ - if (m_nFirstVisibleRowOnList < m_nTotalListRow - MAX_VISIBLE_LIST_ROW) { \ - m_nSelectedListRow++; \ - m_nFirstVisibleRowOnList++; \ - m_nCurListItemY += LIST_HEIGHT / m_nTotalListRow; \ - } \ - } else { \ - if (m_nSelectedListRow < m_nTotalListRow - 1) { \ - m_nSelectedListRow++; \ - } \ - } \ - } while(0) +inline void +CMenuManager::ScrollUpListByOne() +{ + if (m_nSelectedListRow == m_nFirstVisibleRowOnList) { + if (m_nFirstVisibleRowOnList > 0) { + m_nSelectedListRow--; + m_nFirstVisibleRowOnList--; + m_nCurListItemY -= LIST_HEIGHT / m_nTotalListRow; + } + } else { + m_nSelectedListRow--; + } +} -#define PageUpList(playSoundOnSuccess) \ - do { \ - if (m_nTotalListRow > MAX_VISIBLE_LIST_ROW) { \ - if (m_nFirstVisibleRowOnList > 0) { \ - if(playSoundOnSuccess) \ - DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); \ - \ - m_nFirstVisibleRowOnList = max(0, m_nFirstVisibleRowOnList - MAX_VISIBLE_LIST_ROW); \ - m_nSelectedListRow = min(m_nSelectedListRow, m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW - 1); \ - } else { \ - m_nFirstVisibleRowOnList = 0; \ - m_nSelectedListRow = 0; \ - } \ - m_nCurListItemY = (LIST_HEIGHT / m_nTotalListRow) * m_nFirstVisibleRowOnList; \ - } \ - } while(0) +inline void +CMenuManager::ScrollDownListByOne() +{ + if (m_nSelectedListRow == m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW - 1) { + if (m_nFirstVisibleRowOnList < m_nTotalListRow - MAX_VISIBLE_LIST_ROW) { + m_nSelectedListRow++; + m_nFirstVisibleRowOnList++; + m_nCurListItemY += LIST_HEIGHT / m_nTotalListRow; + } + } else { + if (m_nSelectedListRow < m_nTotalListRow - 1) { + m_nSelectedListRow++; + } + } +} -#define PageDownList(playSoundOnSuccess) \ - do { \ - if (m_nTotalListRow > MAX_VISIBLE_LIST_ROW) { \ - if (m_nFirstVisibleRowOnList < m_nTotalListRow - MAX_VISIBLE_LIST_ROW) { \ - if(playSoundOnSuccess) \ - DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); \ - \ - m_nFirstVisibleRowOnList = min(m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW, m_nTotalListRow - MAX_VISIBLE_LIST_ROW); \ - m_nSelectedListRow = max(m_nSelectedListRow, m_nFirstVisibleRowOnList); \ - } else { \ - m_nFirstVisibleRowOnList = m_nTotalListRow - MAX_VISIBLE_LIST_ROW; \ - m_nSelectedListRow = m_nTotalListRow - 1; \ - } \ - m_nCurListItemY = (LIST_HEIGHT / m_nTotalListRow) * m_nFirstVisibleRowOnList; \ - } \ - } while(0) +inline void +CMenuManager::PageUpList(bool playSoundOnSuccess) +{ + if (m_nTotalListRow > MAX_VISIBLE_LIST_ROW) { + if (m_nFirstVisibleRowOnList > 0) { + if(playSoundOnSuccess) + DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); + + m_nFirstVisibleRowOnList = max(0, m_nFirstVisibleRowOnList - MAX_VISIBLE_LIST_ROW); + m_nSelectedListRow = min(m_nSelectedListRow, m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW - 1); + } else { + m_nFirstVisibleRowOnList = 0; + m_nSelectedListRow = 0; + } + m_nCurListItemY = (LIST_HEIGHT / m_nTotalListRow) * m_nFirstVisibleRowOnList; + } +} + +inline void +CMenuManager::PageDownList(bool playSoundOnSuccess) +{ + if (m_nTotalListRow > MAX_VISIBLE_LIST_ROW) { + if (m_nFirstVisibleRowOnList < m_nTotalListRow - MAX_VISIBLE_LIST_ROW) { + if(playSoundOnSuccess) + DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); + + m_nFirstVisibleRowOnList = min(m_nFirstVisibleRowOnList + MAX_VISIBLE_LIST_ROW, m_nTotalListRow - MAX_VISIBLE_LIST_ROW); + m_nSelectedListRow = max(m_nSelectedListRow, m_nFirstVisibleRowOnList); + } else { + m_nFirstVisibleRowOnList = m_nTotalListRow - MAX_VISIBLE_LIST_ROW; + m_nSelectedListRow = m_nTotalListRow - 1; + } + m_nCurListItemY = (LIST_HEIGHT / m_nTotalListRow) * m_nFirstVisibleRowOnList; + } +} + +inline void +CMenuManager::ThingsToDoBeforeLeavingPage() +{ + if ((m_nCurrScreen == MENUPAGE_SKIN_SELECT) && strcmp(m_aSkinName, m_PrefsSkinFile) != 0) { + CWorld::Players[0].SetPlayerSkin(m_PrefsSkinFile); + } else if (m_nCurrScreen == MENUPAGE_SOUND_SETTINGS) { + if (m_nPrefsAudio3DProviderIndex != -1) + m_nPrefsAudio3DProviderIndex = DMAudio.GetCurrent3DProviderIndex(); +#ifdef TIDY_UP_PBP + DMAudio.StopFrontEndTrack(); + OutputDebugString("FRONTEND AUDIO TRACK STOPPED"); +#endif + } else if (m_nCurrScreen == MENUPAGE_GRAPHICS_SETTINGS) { + m_nDisplayVideoMode = m_nPrefsVideoMode; + } + + if (m_nCurrScreen == MENUPAGE_SKIN_SELECT) { + CPlayerSkin::EndFrontendSkinEdit(); + } + + if ((m_nCurrScreen == MENUPAGE_SKIN_SELECT) || (m_nCurrScreen == MENUPAGE_KEYBOARD_CONTROLS)) { + m_nTotalListRow = 0; + } +} + +// ------ Functions not in the game/inlined ends void CMenuManager::BuildStatLine(char *text, void *stat, uint8 aFloat, void *stat2) @@ -1173,7 +1207,6 @@ void CMenuManager::DrawFrontEnd() bbNames[5] = { "FESZ_QU",MENUPAGE_EXIT }; bbTabCount = 6; } - m_nCurrScreen = MENUPAGE_NEW_GAME; } else { if (bbTabCount != 8) { bbNames[0] = { "FEB_STA",MENUPAGE_STATS }; @@ -1186,8 +1219,8 @@ void CMenuManager::DrawFrontEnd() bbNames[7] = { "FESZ_QU",MENUPAGE_EXIT }; bbTabCount = 8; } - m_nCurrScreen = MENUPAGE_STATS; } + m_nCurrScreen = bbNames[0].screenId; bottomBarActive = true; curBottomBarOption = 0; } @@ -1285,7 +1318,6 @@ void CMenuManager::DrawFrontEndNormal() eFrontendSprites currentSprite; switch (m_nCurrScreen) { case MENUPAGE_STATS: - case MENUPAGE_NEW_GAME: case MENUPAGE_START_MENU: case MENUPAGE_PAUSE_MENU: case MENUPAGE_EXIT: @@ -1315,7 +1347,7 @@ void CMenuManager::DrawFrontEndNormal() currentSprite = FE_ICONCONTROLS; break; default: - /* actually MENUPAGE_NEW_GAME too*/ + /*case MENUPAGE_NEW_GAME: */ /*case MENUPAGE_BRIEFS: */ currentSprite = FE_ICONBRIEF; break; @@ -1324,16 +1356,16 @@ void CMenuManager::DrawFrontEndNormal() m_aFrontEndSprites[currentSprite].Draw(CRect(MENU_X_LEFT_ALIGNED(50.0f), MENU_Y(50.0f), MENU_X_RIGHT_ALIGNED(50.0f), SCREEN_SCALE_FROM_BOTTOM(95.0f)), CRGBA(255, 255, 255, m_nMenuFadeAlpha > 255 ? 255 : m_nMenuFadeAlpha)); if (m_nMenuFadeAlpha < 255) { - static int LastFade = 0; + static uint32 LastFade = 0; if (m_nMenuFadeAlpha <= 0 && reverseAlpha) { reverseAlpha = false; ChangeScreen(pendingScreen, pendingOption, true, false); - } else if(CTimer::GetTimeInMillisecondsPauseMode() - LastFade > 10){ + } else { if (!reverseAlpha) - m_nMenuFadeAlpha += 20; + m_nMenuFadeAlpha += min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 20.0f; else - m_nMenuFadeAlpha = max(m_nMenuFadeAlpha - 30, 0); + m_nMenuFadeAlpha = max(0, m_nMenuFadeAlpha - min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 30.0f); LastFade = CTimer::GetTimeInMillisecondsPauseMode(); } @@ -1537,12 +1569,18 @@ void CMenuManager::DrawFrontEndNormal() } if (m_nMenuFadeAlpha < 255) { - static int LastFade = 0; + static uint32 LastFade = 0; + // Famous transparent menu bug. 33.0f = 1000.f/30.f (original frame limiter fps) +#ifdef FIX_BUGS + m_nMenuFadeAlpha += min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 20.0f; + LastFade = CTimer::GetTimeInMillisecondsPauseMode(); +#else if(CTimer::GetTimeInMillisecondsPauseMode() - LastFade > 10){ m_nMenuFadeAlpha += 20; LastFade = CTimer::GetTimeInMillisecondsPauseMode(); } +#endif if (m_nMenuFadeAlpha > 255){ m_aMenuSprites[currentSprite].Draw(CRect(0.0f, 0.0f, SCREEN_WIDTH, SCREEN_HEIGHT), CRGBA(255, 255, 255, 255)); @@ -1950,7 +1988,7 @@ WRAPPER void CMenuManager::Process(void) { EAXJMP(0x485100); } #else void CMenuManager::Process(void) { - m_bMenuNotProcessed = false; + m_bMenuStateChanged = false; if (!m_bSaveMenuActive && TheCamera.GetScreenFadeStatus() != FADE_0) return; @@ -2701,6 +2739,8 @@ CMenuManager::ProcessButtonPresses(void) if (CPad::GetPad(0)->GetEnterJustDown() || CPad::GetPad(0)->GetCrossJustDown()) { DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); bottomBarActive = false; + + // If there's a menu change with fade ongoing, finish it now if (reverseAlpha) m_nMenuFadeAlpha = 0; return; @@ -3116,51 +3156,43 @@ CMenuManager::ProcessButtonPresses(void) if (goBack) { CMenuManager::ResetHelperText(); DMAudio.PlayFrontEndSound(SOUND_FRONTEND_EXIT, 0); - if (m_nCurrScreen == MENUPAGE_PAUSE_MENU && !m_bGameNotLoaded && !m_bMenuNotProcessed){ - if (CMenuManager::m_PrefsVsyncDisp != CMenuManager::m_PrefsVsync) { - CMenuManager::m_PrefsVsync = CMenuManager::m_PrefsVsyncDisp; - } - CMenuManager::RequestFrontEndShutDown(); - } else if (m_nCurrScreen == MENUPAGE_CHOOSE_SAVE_SLOT -#ifdef PS2_SAVE_DIALOG - || m_nCurrScreen == MENUPAGE_SAVE -#endif - ) { - CMenuManager::RequestFrontEndShutDown(); - } else if (m_nCurrScreen == MENUPAGE_SOUND_SETTINGS) { - DMAudio.StopFrontEndTrack(); - OutputDebugString("FRONTEND AUDIO TRACK STOPPED"); - } - - int oldScreen = !m_bGameNotLoaded ? aScreens[m_nCurrScreen].m_PreviousPage[1] : aScreens[m_nCurrScreen].m_PreviousPage[0]; - int oldOption = !m_bGameNotLoaded ? aScreens[m_nCurrScreen].m_ParentEntry[1] : aScreens[m_nCurrScreen].m_ParentEntry[0]; - #ifdef PS2_LIKE_MENU - if (bottomBarActive){ - bottomBarActive = false; - if (!m_bGameNotLoaded) { + if (m_nCurrScreen == MENUPAGE_PAUSE_MENU || bottomBarActive) { +#else + if (m_nCurrScreen == MENUPAGE_PAUSE_MENU) { +#endif + if (!m_bGameNotLoaded && !m_bMenuStateChanged) { if (CMenuManager::m_PrefsVsyncDisp != CMenuManager::m_PrefsVsync) { CMenuManager::m_PrefsVsync = CMenuManager::m_PrefsVsyncDisp; } CMenuManager::RequestFrontEndShutDown(); } + + // We're already resuming, we don't need further processing. +#if defined(FIX_BUGS) || defined(PS2_LIKE_MENU) return; +#endif + } +#ifdef PS2_LIKE_MENU + else if (m_nCurrScreen == MENUPAGE_CHOOSE_SAVE_SLOT || m_nCurrScreen == MENUPAGE_SAVE) { +#else + else if (m_nCurrScreen == MENUPAGE_CHOOSE_SAVE_SLOT) { +#endif + CMenuManager::RequestFrontEndShutDown(); + } + // It's now in ThingsToDoBeforeLeavingPage() +#ifndef TIDY_UP_PBP + else if (m_nCurrScreen == MENUPAGE_SOUND_SETTINGS) { + DMAudio.StopFrontEndTrack(); + OutputDebugString("FRONTEND AUDIO TRACK STOPPED"); } #endif + int oldScreen = !m_bGameNotLoaded ? aScreens[m_nCurrScreen].m_PreviousPage[1] : aScreens[m_nCurrScreen].m_PreviousPage[0]; + int oldOption = !m_bGameNotLoaded ? aScreens[m_nCurrScreen].m_ParentEntry[1] : aScreens[m_nCurrScreen].m_ParentEntry[0]; + if (oldScreen != -1) { - if ((m_nCurrScreen == MENUPAGE_SKIN_SELECT) && strcmp(m_aSkinName, m_PrefsSkinFile) != 0) { - CWorld::Players[0].SetPlayerSkin(m_PrefsSkinFile); - } - if ((m_nCurrScreen == MENUPAGE_SOUND_SETTINGS) && (m_nPrefsAudio3DProviderIndex != -1)) { - m_nPrefsAudio3DProviderIndex = DMAudio.GetCurrent3DProviderIndex(); - } - if (m_nCurrScreen == MENUPAGE_GRAPHICS_SETTINGS) { - m_nDisplayVideoMode = m_nPrefsVideoMode; - } - if (m_nCurrScreen == MENUPAGE_SKIN_SELECT) { - CPlayerSkin::EndFrontendSkinEdit(); - } + ThingsToDoBeforeLeavingPage(); #ifdef PS2_LIKE_MENU if (!bottomBarActive && @@ -3168,10 +3200,8 @@ CMenuManager::ProcessButtonPresses(void) bottomBarActive = true; } else #endif + { ChangeScreen(oldScreen, oldOption, true, true); - - if ((m_nPrevScreen == MENUPAGE_SKIN_SELECT) || (m_nPrevScreen == MENUPAGE_KEYBOARD_CONTROLS)) { - m_nTotalListRow = 0; } // We will go back for sure at this point, why process other things?! @@ -3512,11 +3542,16 @@ WRAPPER void CMenuManager::SwitchMenuOnAndOff() { EAXJMP(0x488790); } #else void CMenuManager::SwitchMenuOnAndOff() { - if (!!(CPad::GetPad(0)->NewState.Start && !CPad::GetPad(0)->OldState.Start) - || m_bShutDownFrontEndRequested || m_bStartUpFrontEndRequested) { + bool menuWasActive = !!m_bMenuActive; - if (!m_bMenuActive) - m_bMenuActive = true; + // Reminder: You need REGISTER_START_BUTTON defined to make it work. + if (CPad::GetPad(0)->GetStartJustDown() +#ifdef FIX_BUGS + && !m_bGameNotLoaded +#endif + || m_bShutDownFrontEndRequested || m_bStartUpFrontEndRequested) { + + m_bMenuActive = !m_bMenuActive; if (m_bShutDownFrontEndRequested) m_bMenuActive = false; @@ -3525,8 +3560,13 @@ void CMenuManager::SwitchMenuOnAndOff() if (m_bMenuActive) { CTimer::StartUserPause(); - } - else { + } else { +#ifdef PS2_LIKE_MENU + bottomBarActive = false; +#endif +#ifdef FIX_BUGS + ThingsToDoBeforeLeavingPage(); +#endif ShutdownJustMenu(); SaveSettings(); m_bStartUpFrontEndRequested = false; @@ -3553,7 +3593,7 @@ void CMenuManager::SwitchMenuOnAndOff() PcSaveHelper.PopulateSlotInfo(); m_nCurrOption = 0; } -/* // Unused? +/* // PS2 leftover? if (m_nCurrScreen != MENUPAGE_SOUND_SETTINGS && gMusicPlaying) { DMAudio.StopFrontEndTrack(); @@ -3561,8 +3601,8 @@ void CMenuManager::SwitchMenuOnAndOff() gMusicPlaying = 0; } */ - if (!m_bMenuActive) - m_bMenuNotProcessed = true; + if (m_bMenuActive != menuWasActive) + m_bMenuStateChanged = true; m_bStartUpFrontEndRequested = false; m_bShutDownFrontEndRequested = false; diff --git a/src/core/Frontend.h b/src/core/Frontend.h index 6d7327d3..3dbed164 100644 --- a/src/core/Frontend.h +++ b/src/core/Frontend.h @@ -403,7 +403,7 @@ public: int32 m_nHelperTextMsgId; bool m_bLanguageLoaded; bool m_bMenuActive; - bool m_bMenuNotProcessed; + bool m_bMenuStateChanged; bool m_bWaitingForNewKeyBind; bool m_bStartGameLoading; bool m_bFirstTime; @@ -540,8 +540,14 @@ public: void WaitForUserCD(); void PrintController(); - // New content: - uint8 GetNumberOfMenuOptions(); + // New (not in function or inlined in the game) + void ThingsToDoBeforeLeavingPage(); + void ScrollUpListByOne(); + void ScrollDownListByOne(); + void PageUpList(bool); + void PageDownList(bool); + + // uint8 GetNumberOfMenuOptions(); }; static_assert(sizeof(CMenuManager) == 0x564, "CMenuManager: error"); diff --git a/src/core/Timer.h b/src/core/Timer.h index 89c4a430..ef525be7 100644 --- a/src/core/Timer.h +++ b/src/core/Timer.h @@ -2,7 +2,7 @@ class CTimer { -public: + static uint32 &m_snTimeInMilliseconds; static uint32 &m_snTimeInMillisecondsPauseMode; static uint32 &m_snTimeInMillisecondsNonClipped; @@ -11,19 +11,20 @@ public: static float &ms_fTimeScale; static float &ms_fTimeStep; static float &ms_fTimeStepNonClipped; +public: static bool &m_UserPause; static bool &m_CodePause; - static float GetTimeStep(void) { return ms_fTimeStep; } + static const float &GetTimeStep(void) { return ms_fTimeStep; } static void SetTimeStep(float ts) { ms_fTimeStep = ts; } static float GetTimeStepInSeconds() { return ms_fTimeStep / 50.0f; } static float GetTimeStepInMilliseconds() { return ms_fTimeStep / 50.0f * 1000.0f; } - static float GetTimeStepNonClipped(void) { return ms_fTimeStepNonClipped; } + static const float &GetTimeStepNonClipped(void) { return ms_fTimeStepNonClipped; } static float GetTimeStepNonClippedInSeconds(void) { return ms_fTimeStepNonClipped / 50.0f; } static void SetTimeStepNonClipped(float ts) { ms_fTimeStepNonClipped = ts; } - static uint32 GetFrameCounter(void) { return m_FrameCounter; } + static const uint32 &GetFrameCounter(void) { return m_FrameCounter; } static void SetFrameCounter(uint32 fc) { m_FrameCounter = fc; } - static uint32 GetTimeInMilliseconds(void) { return m_snTimeInMilliseconds; } + static const uint32 &GetTimeInMilliseconds(void) { return m_snTimeInMilliseconds; } static void SetTimeInMilliseconds(uint32 t) { m_snTimeInMilliseconds = t; } static uint32 GetTimeInMillisecondsNonClipped(void) { return m_snTimeInMillisecondsNonClipped; } static void SetTimeInMillisecondsNonClipped(uint32 t) { m_snTimeInMillisecondsNonClipped = t; } @@ -31,7 +32,7 @@ public: static void SetTimeInMillisecondsPauseMode(uint32 t) { m_snTimeInMillisecondsPauseMode = t; } static uint32 GetPreviousTimeInMilliseconds(void) { return m_snPreviousTimeInMilliseconds; } static void SetPreviousTimeInMilliseconds(uint32 t) { m_snPreviousTimeInMilliseconds = t; } - static float GetTimeScale(void) { return ms_fTimeScale; } + static const float &GetTimeScale(void) { return ms_fTimeScale; } static void SetTimeScale(float ts) { ms_fTimeScale = ts; } static bool GetIsPaused() { return m_UserPause || m_CodePause; } diff --git a/src/core/config.h b/src/core/config.h index 9235e744..ff779946 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -101,6 +101,8 @@ enum Config { NUMPEDGROUPS = 31, NUMMODELSPERPEDGROUP = 8, + NUMROADBLOCKS = 600, + NUMVISIBLEENTITIES = 2000, NUMINVISIBLEENTITIES = 150, @@ -169,10 +171,11 @@ enum Config { #endif #define FIX_BUGS // fixes bugs that we've came across during reversing, TODO: use this more -#define TOGGLEABLE_BETA_FEATURES // toggleable from debug menu. doesn't have too many things +#define TOGGLEABLE_BETA_FEATURES // toggleable from debug menu. not too many things // Pad #define KANGAROO_CHEAT +#define REGISTER_START_BUTTON // currently only in menu sadly. resumes the game // Hud, frontend and radar #define ASPECT_RATIO_SCALE // Not just makes everything scale with aspect ratio, also adds support for all aspect ratios @@ -199,5 +202,5 @@ enum Config { // Peds #define ANIMATE_PED_COL_MODEL #define VC_PED_PORTS // various ports from VC's CPed, mostly subtle -#define NEW_WALK_AROUND_ALGORITHM // to make walking around vehicles/objects less awkward +// #define NEW_WALK_AROUND_ALGORITHM // to make walking around vehicles/objects less awkward #define CANCELLABLE_CAR_ENTER diff --git a/src/peds/CopPed.cpp b/src/peds/CopPed.cpp index 53ae1747..dae866a4 100644 --- a/src/peds/CopPed.cpp +++ b/src/peds/CopPed.cpp @@ -7,8 +7,11 @@ #include "Vehicle.h" #include "RpAnimBlend.h" #include "General.h" +#include "ZoneCull.h" +#include "PathFind.h" +#include "RoadBlocks.h" -WRAPPER void CCopPed::ProcessControl() { EAXJMP(0x4C1400); } +WRAPPER void CCopPed::ProcessControl() { EAXJMP(0x4C1400); } CCopPed::CCopPed(eCopType copType) : CPed(PEDTYPE_COP) { @@ -58,11 +61,16 @@ CCopPed::CCopPed(eCopType copType) : CPed(PEDTYPE_COP) m_bIsDisabledCop = false; field_1356 = 0; m_attackTimer = 0; - field_1351 = 0; + m_bBeatingSuspect = false; m_bZoneDisabledButClose = false; m_bZoneDisabled = false; field_1364 = -1; m_pPointGunAt = nil; + + // VC also initializes in here, but it keeps object +#ifdef FIX_BUGS + m_wRoadblockNode = -1; +#endif } CCopPed::~CCopPed() @@ -181,15 +189,15 @@ CCopPed::ClearPursuit(void) } } -// TO-DO: m_MaxCops in for loop may be a bug, check it out after CopAI +// TODO: I don't know why they needed that parameter. void -CCopPed::SetPursuit(bool iMayAlreadyBeInPursuit) +CCopPed::SetPursuit(bool ignoreCopLimit) { CWanted *wanted = FindPlayerPed()->m_pWanted; if (m_bIsInPursuit || !IsPedInControl()) return; - if (wanted->m_CurrentCops < wanted->m_MaxCops || iMayAlreadyBeInPursuit) { + if (wanted->m_CurrentCops < wanted->m_MaxCops || ignoreCopLimit) { for (int i = 0; i < wanted->m_MaxCops; ++i) { if (!wanted->m_pCops[i]) { m_bIsInPursuit = true; @@ -275,6 +283,274 @@ CCopPed::ScanForCrimes(void) } } +void +CCopPed::CopAI(void) +{ + CWanted *wanted = FindPlayerPed()->m_pWanted; + int wantedLevel = wanted->m_nWantedLevel; + CPhysical *playerOrHisVeh = FindPlayerVehicle() ? (CPhysical*)FindPlayerVehicle() : (CPhysical*)FindPlayerPed(); + + if (wanted->m_bIgnoredByEveryone || wanted->m_bIgnoredByCops) { + if (m_nPedState != PED_ARREST_PLAYER) + ClearPursuit(); + + return; + } + if (CCullZones::NoPolice() && m_bIsInPursuit && !m_bIsDisabledCop) { + if (bHitSomethingLastFrame) { + m_bZoneDisabled = true; + m_bIsDisabledCop = true; +#ifdef FIX_BUGS + m_wRoadblockNode = -1; +#else + m_wRoadblockNode = 0; +#endif + bKindaStayInSamePlace = true; + bIsRunning = false; + bNotAllowedToDuck = false; + bCrouchWhenShooting = false; + SetIdle(); + ClearObjective(); + ClearPursuit(); + m_prevObjective = OBJECTIVE_NONE; + m_nLastPedState = PED_NONE; + SetAttackTimer(0); + if (m_fDistanceToTarget > 15.0f) + m_bZoneDisabledButClose = true; + } + } else if (m_bZoneDisabled && !CCullZones::NoPolice()) { + m_bZoneDisabled = false; + m_bIsDisabledCop = false; + m_bZoneDisabledButClose = false; + bKindaStayInSamePlace = false; + bCrouchWhenShooting = false; + bDuckAndCover = false; + ClearPursuit(); + } + if (wantedLevel > 0) { + if (!m_bIsDisabledCop) { + if (!m_bIsInPursuit || wanted->m_CurrentCops > wanted->m_MaxCops) { + CCopPed *copFarthestToTarget = nil; + float copFarthestToTargetDist = m_fDistanceToTarget; + + int oldCopNum = wanted->m_CurrentCops; + int maxCops = wanted->m_MaxCops; + + for (int i = 0; i < max(maxCops, oldCopNum); i++) { + CCopPed *cop = wanted->m_pCops[i]; + if (cop && cop->m_fDistanceToTarget > copFarthestToTargetDist) { + copFarthestToTargetDist = cop->m_fDistanceToTarget; + copFarthestToTarget = wanted->m_pCops[i]; + } + } + + if (m_bIsInPursuit) { + if (copFarthestToTarget && oldCopNum > maxCops) { + if (copFarthestToTarget == this && m_fDistanceToTarget > 10.0f) { + ClearPursuit(); + } else if(copFarthestToTargetDist > 10.0f) + copFarthestToTarget->ClearPursuit(); + } + } else { + if (oldCopNum < maxCops) { + SetPursuit(true); + } else { + if (m_fDistanceToTarget <= 10.0f || copFarthestToTarget && m_fDistanceToTarget < copFarthestToTargetDist) { + if (copFarthestToTarget && copFarthestToTargetDist > 10.0f) + copFarthestToTarget->ClearPursuit(); + + SetPursuit(true); + } + } + } + } else + SetPursuit(false); + + if (!m_bIsInPursuit) + return; + + if (wantedLevel > 1 && GetWeapon()->m_eWeaponType == WEAPONTYPE_UNARMED) + SetCurrentWeapon(WEAPONTYPE_COLT45); + else if (wantedLevel == 1 && GetWeapon()->m_eWeaponType != WEAPONTYPE_UNARMED && !FindPlayerPed()->m_pCurrentPhysSurface) { + // i.e. if player is on top of car, cop will still use colt45. + SetCurrentWeapon(WEAPONTYPE_UNARMED); + } + + if (FindPlayerVehicle()) { + if (m_bBeatingSuspect) { + --wanted->m_CopsBeatingSuspect; + m_bBeatingSuspect = false; + } + if (m_fDistanceToTarget * FindPlayerSpeed().Magnitude() > 4.0f) + ClearPursuit(); + } + return; + } + float weaponRange = CWeaponInfo::GetWeaponInfo(GetWeapon()->m_eWeaponType)->m_fRange; + SetLookFlag(playerOrHisVeh, true); + TurnBody(); + SetCurrentWeapon(WEAPONTYPE_COLT45); + if (!bIsDucking) { + if (m_attackTimer >= CTimer::GetTimeInMilliseconds()) { + if (m_nPedState != PED_ATTACK && m_nPedState != PED_FIGHT && !m_bZoneDisabled) { + CVector targetDist = playerOrHisVeh->GetPosition() - GetPosition(); + if (m_fDistanceToTarget > 30.0f) { + CAnimBlendAssociation* crouchShootAssoc = RpAnimBlendClumpGetAssociation(GetClump(), ANIM_RBLOCK_CSHOOT); + if (crouchShootAssoc) + crouchShootAssoc->blendDelta = -1000.0f; + + // Target is coming onto us + if (DotProduct(playerOrHisVeh->m_vecMoveSpeed, targetDist) > 0.0f) { + m_bIsDisabledCop = false; + bKindaStayInSamePlace = false; + bNotAllowedToDuck = false; + bDuckAndCover = false; + SetPursuit(false); + SetObjective(OBJECTIVE_KILL_CHAR_ANY_MEANS, FindPlayerPed()); + } + } else if (m_fDistanceToTarget < 5.0f + && (!FindPlayerVehicle() || FindPlayerVehicle()->m_vecMoveSpeed.MagnitudeSqr() < sq(1.f/200.f))) { + m_bIsDisabledCop = false; + bKindaStayInSamePlace = false; + bNotAllowedToDuck = false; + bDuckAndCover = false; + } else { + // VC checks for != nil compared to buggy behaviour of III. I check for != -1 here. +#ifdef VC_PED_PORTS + float dotProd; + if (m_wRoadblockNode != -1) { + CTreadable *roadBlockRoad = ThePaths.m_mapObjects[CRoadBlocks::RoadBlockObjects[m_wRoadblockNode]]; + dotProd = DotProduct2D(playerOrHisVeh->GetPosition() - roadBlockRoad->GetPosition(), GetPosition() - roadBlockRoad->GetPosition()); + } else + dotProd = -1.0f; + + if(dotProd >= 0.0f) { +#else + +#ifndef FIX_BUGS + float copRoadDotProd, targetRoadDotProd; +#else + float copRoadDotProd = 1.0f, targetRoadDotProd = 1.0f; + if (m_wRoadblockNode != -1) +#endif + { + CTreadable* roadBlockRoad = ThePaths.m_mapObjects[CRoadBlocks::RoadBlockObjects[m_wRoadblockNode]]; + CVector2D roadFwd = roadBlockRoad->GetForward(); + copRoadDotProd = DotProduct2D(GetPosition() - roadBlockRoad->GetPosition(), roadFwd); + targetRoadDotProd = DotProduct2D(playerOrHisVeh->GetPosition() - roadBlockRoad->GetPosition(), roadFwd); + } + // Roadblock may be towards road's fwd or opposite, so check both + if ((copRoadDotProd >= 0.0f || targetRoadDotProd >= 0.0f) + && (copRoadDotProd <= 0.0f || targetRoadDotProd <= 0.0f)) { +#endif + bIsPointingGunAt = true; + } else { + m_bIsDisabledCop = false; + bKindaStayInSamePlace = false; + bNotAllowedToDuck = false; + bCrouchWhenShooting = false; + bIsDucking = false; + bDuckAndCover = false; + SetPursuit(false); + } + } + } + } else { + if (m_fDistanceToTarget < weaponRange) { + CWeaponInfo *weaponInfo = CWeaponInfo::GetWeaponInfo(GetWeapon()->m_eWeaponType); + CVector gunPos = weaponInfo->m_vecFireOffset; + for (RwFrame *i = GetNodeFrame(PED_HANDR); i; i = RwFrameGetParent(i)) + RwV3dTransformPoints((RwV3d*)&gunPos, (RwV3d*)&gunPos, 1, RwFrameGetMatrix(i)); + + CColPoint foundCol; + CEntity *foundEnt; + if (!CWorld::ProcessLineOfSight(gunPos, playerOrHisVeh->GetPosition(), foundCol, foundEnt, + false, true, false, false, true, false, false) + || foundEnt && foundEnt == playerOrHisVeh) { + m_pPointGunAt = playerOrHisVeh; + if (playerOrHisVeh) + playerOrHisVeh->RegisterReference((CEntity**) &m_pPointGunAt); + + SetAttack(playerOrHisVeh); + SetShootTimer(CGeneral::GetRandomNumberInRange(500, 1000)); + } + SetAttackTimer(CGeneral::GetRandomNumberInRange(100, 300)); + } + SetMoveState(PEDMOVE_STILL); + } + } + } else { + if (!m_bIsDisabledCop || m_bZoneDisabled) { + if (m_nPedState != PED_AIM_GUN) { + if (m_bIsInPursuit) + ClearPursuit(); + + if (IsPedInControl()) { + // Entering the vehicle + if (m_pMyVehicle && !bInVehicle) { + if (m_pMyVehicle->IsLawEnforcementVehicle()) { + if (m_pMyVehicle->pDriver) { + if (m_pMyVehicle->pDriver->m_nPedType == PEDTYPE_COP) { + if (m_objective != OBJECTIVE_ENTER_CAR_AS_PASSENGER) + SetObjective(OBJECTIVE_ENTER_CAR_AS_PASSENGER, m_pMyVehicle); + } else if (m_pMyVehicle->pDriver->IsPlayer()) { + FindPlayerPed()->SetWantedLevelNoDrop(1); + } + } else if (m_objective != OBJECTIVE_ENTER_CAR_AS_DRIVER) { + SetObjective(OBJECTIVE_ENTER_CAR_AS_DRIVER, m_pMyVehicle); + } + } else { + m_pMyVehicle = nil; + ClearObjective(); + SetWanderPath(CGeneral::GetRandomNumber() & 7); + } + } +#ifdef VC_PED_PORTS + else { + if (m_objective != OBJECTIVE_KILL_CHAR_ON_FOOT && CharCreatedBy == RANDOM_CHAR) { + for (int i = 0; i < m_numNearPeds; i++) { + CPed *nearPed = m_nearPeds[i]; + if (nearPed->CharCreatedBy == RANDOM_CHAR) { + if ((nearPed->m_nPedType == PEDTYPE_CRIMINAL || nearPed->IsGangMember()) + && nearPed->IsPedInControl()) { + + bool anotherCopChasesHim = false; + if (nearPed->m_nPedState == PED_FLEE_ENTITY) { + if (nearPed->m_fleeFrom && nearPed->m_fleeFrom->IsPed() && + ((CPed*)nearPed->m_fleeFrom)->m_nPedType == PEDTYPE_COP) { + anotherCopChasesHim = true; + } + } + if (!anotherCopChasesHim) { + SetObjective(OBJECTIVE_KILL_CHAR_ON_FOOT, nearPed); + nearPed->SetObjective(OBJECTIVE_FLEE_CHAR_ON_FOOT_TILL_SAFE, this); + nearPed->m_ped_flagE2 = true; + return; + } + } + } + } + } + } +#endif + } + } + } else { + if (m_bIsInPursuit && m_nPedState != PED_AIM_GUN) + ClearPursuit(); + + m_bIsDisabledCop = false; + bKindaStayInSamePlace = false; + bNotAllowedToDuck = false; + bCrouchWhenShooting = false; + bIsDucking = false; + bDuckAndCover = false; + if (m_pMyVehicle) + SetObjective(OBJECTIVE_ENTER_CAR_AS_DRIVER, m_pMyVehicle); + } + } +} + class CCopPed_ : public CCopPed { public: @@ -290,4 +566,5 @@ STARTPATCHES InjectHook(0x4C27D0, &CCopPed::SetPursuit, PATCH_JUMP); InjectHook(0x4C2C90, &CCopPed::ArrestPlayer, PATCH_JUMP); InjectHook(0x4C26A0, &CCopPed::ScanForCrimes, PATCH_JUMP); + InjectHook(0x4C1B50, &CCopPed::CopAI, PATCH_JUMP); ENDPATCHES diff --git a/src/peds/CopPed.h b/src/peds/CopPed.h index 7705eb12..142be56a 100644 --- a/src/peds/CopPed.h +++ b/src/peds/CopPed.h @@ -17,9 +17,9 @@ public: int8 field_1343; float m_fDistanceToTarget; int8 m_bIsInPursuit; - int8 m_bIsDisabledCop; + int8 m_bIsDisabledCop; // What disabled cop actually is? int8 field_1350; - int8 field_1351; + bool m_bBeatingSuspect; int8 m_bZoneDisabledButClose; int8 m_bZoneDisabled; int8 field_1354; @@ -40,6 +40,7 @@ public: void SetPursuit(bool); void ArrestPlayer(void); void ScanForCrimes(void); + void CopAI(void); }; static_assert(sizeof(CCopPed) == 0x558, "CCopPed: error"); diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index db6b7ee2..ae24faa3 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -2720,6 +2720,10 @@ CPed::SetObjective(eObjective newObj, void *entity) return; } +#ifdef VC_PED_PORTS + SetObjectiveTimer(0); + ClearPointGunAt(); +#endif bObjectiveCompleted = false; if (!IsTemporaryObjective(m_objective) || IsTemporaryObjective(newObj)) { if (m_objective != newObj) { @@ -3444,8 +3448,12 @@ CPed::ClearAll(void) m_fleeFrom = nil; m_fleeTimer = 0; bUsesCollision = true; +#ifdef VC_PED_PORTS + ClearPointGunAt(); +#else ClearAimFlag(); ClearLookFlag(); +#endif bIsPointingGunAt = false; bRenderPedInCar = true; bKnockedUpIntoAir = false; diff --git a/src/peds/Population.cpp b/src/peds/Population.cpp index d87764ff..6b674dd3 100644 --- a/src/peds/Population.cpp +++ b/src/peds/Population.cpp @@ -576,7 +576,7 @@ CPopulation::AddToPopulation(float minDist, float maxDist, float minDistOffScree } // Yeah, float float maxPossiblePedsForArea = (zoneInfo.pedDensity + zoneInfo.carDensity) * playerInfo->m_fRoadDensity * PedDensityMultiplier * CIniFile::PedNumberMultiplier; - // maxPossiblePedsForArea = min(maxPossiblePedsForArea, MaxNumberOfPedsInUse); + maxPossiblePedsForArea = min(maxPossiblePedsForArea, MaxNumberOfPedsInUse); if (ms_nTotalPeds < maxPossiblePedsForArea || addCop) { int decisionThreshold = CGeneral::GetRandomNumberInRange(0, 1000); diff --git a/src/save/GenericGameStorage.cpp b/src/save/GenericGameStorage.cpp index 8c851686..5288e67e 100644 --- a/src/save/GenericGameStorage.cpp +++ b/src/save/GenericGameStorage.cpp @@ -61,9 +61,9 @@ do {\ MakeSpaceForSizeInBufferPointer(presize, buf, postsize);\ save_func(buf, &size);\ CopySizeAndPreparePointer(presize, buf, postsize, reserved, size);\ - if (!PcSaveHelper.PcClassSaveRoutine(file, work_buff, size + 4))\ + if (!PcSaveHelper.PcClassSaveRoutine(file, work_buff, buf - work_buff))\ return false;\ - totalSize += size;\ + totalSize += buf - work_buff;\ } while (0) bool @@ -74,7 +74,6 @@ GenericSave(int file) uint32 reserved; uint32 totalSize; - uint32 i; wchar *lastMissionPassed; wchar suffix[6]; @@ -85,13 +84,11 @@ GenericSave(int file) CheckSum = 0; buf = work_buff; reserved = 0; - totalSize = 0; // Save simple vars -INITSAVEBUF lastMissionPassed = TheText.Get(CStats::LastMissionPassedName); if (*lastMissionPassed) { - AsciiToUnicode("'...", suffix); + AsciiToUnicode("...'", suffix); TextCopy(saveName, lastMissionPassed); int len = UnicodeStrlen(saveName); saveName[len] = '\0'; @@ -104,20 +101,20 @@ INITSAVEBUF WriteDataToBufferPointer(buf, saveTime); WriteDataToBufferPointer(buf, SIZE_OF_ONE_GAME_IN_BYTES); WriteDataToBufferPointer(buf, CGame::currLevel); - WriteDataToBufferPointer(buf, TheCamera.m_matrix.m_matrix.pos.x); - WriteDataToBufferPointer(buf, TheCamera.m_matrix.m_matrix.pos.y); - WriteDataToBufferPointer(buf, TheCamera.m_matrix.m_matrix.pos.z); + WriteDataToBufferPointer(buf, TheCamera.GetPosition().x); + WriteDataToBufferPointer(buf, TheCamera.GetPosition().y); + WriteDataToBufferPointer(buf, TheCamera.GetPosition().z); WriteDataToBufferPointer(buf, CClock::ms_nMillisecondsPerGameMinute); WriteDataToBufferPointer(buf, CClock::ms_nLastClockTick); WriteDataToBufferPointer(buf, CClock::ms_nGameClockHours); WriteDataToBufferPointer(buf, CClock::ms_nGameClockMinutes); currPad = CPad::GetPad(0); WriteDataToBufferPointer(buf, currPad->Mode); - WriteDataToBufferPointer(buf, CTimer::m_snTimeInMilliseconds); - WriteDataToBufferPointer(buf, CTimer::ms_fTimeScale); - WriteDataToBufferPointer(buf, CTimer::ms_fTimeStep); - WriteDataToBufferPointer(buf, CTimer::ms_fTimeStepNonClipped); - WriteDataToBufferPointer(buf, CTimer::m_FrameCounter); + WriteDataToBufferPointer(buf, CTimer::GetTimeInMilliseconds()); + WriteDataToBufferPointer(buf, CTimer::GetTimeScale()); + WriteDataToBufferPointer(buf, CTimer::GetTimeStep()); + WriteDataToBufferPointer(buf, CTimer::GetTimeStepNonClipped()); + WriteDataToBufferPointer(buf, CTimer::GetFrameCounter()); WriteDataToBufferPointer(buf, CTimeStep::ms_fTimeStep); WriteDataToBufferPointer(buf, CTimeStep::ms_fFramesPerUpdate); WriteDataToBufferPointer(buf, CTimeStep::ms_fTimeScale); @@ -134,10 +131,8 @@ INITSAVEBUF WriteDataToBufferPointer(buf, CWeather::WeatherTypeInList); WriteDataToBufferPointer(buf, TheCamera.CarZoomIndicator); WriteDataToBufferPointer(buf, TheCamera.PedZoomIndicator); -#ifdef VALIDATE_SAVE_SIZE - _saveBufCount = buf - work_buff; -#endif -VALIDATESAVEBUF(SIZE_OF_SIMPLEVARS); + + assert(buf - work_buff == SIZE_OF_SIMPLEVARS); // Save scripts, block is nested within the same block as simple vars for some reason presize = buf; @@ -145,9 +140,10 @@ VALIDATESAVEBUF(SIZE_OF_SIMPLEVARS); postsize = buf; CTheScripts::SaveAllScripts(buf, &size); CopySizeAndPreparePointer(presize, buf, postsize, reserved, size); - if (!PcSaveHelper.PcClassSaveRoutine(file, work_buff, size + SIZE_OF_SIMPLEVARS + 4)) + if (!PcSaveHelper.PcClassSaveRoutine(file, work_buff, buf - work_buff)) return false; - totalSize += size + SIZE_OF_SIMPLEVARS; + + totalSize = buf - work_buff; // Save the rest WRITE_BLOCK(CPools::SavePedPool); @@ -171,8 +167,7 @@ VALIDATESAVEBUF(SIZE_OF_SIMPLEVARS); WRITE_BLOCK(CPedType::Save); // Write padding - i = 0; - do { + for (int i = 0; i < 4; i++) { size = align4bytes(SIZE_OF_ONE_GAME_IN_BYTES - totalSize - 4); if (size > sizeof(work_buff)) size = sizeof(work_buff); @@ -181,15 +176,15 @@ VALIDATESAVEBUF(SIZE_OF_SIMPLEVARS); return false; totalSize += size; } - i++; - } while (i < 4); + } // Write checksum and close CFileMgr::Write(file, (const char *) &CheckSum, sizeof(CheckSum)); if (CFileMgr::GetErrorReadWrite(file)) { PcSaveHelper.nErrorCode = SAVESTATUS_ERR_SAVE_WRITE; - if (CloseFile(file)) + if (!CloseFile(file)) PcSaveHelper.nErrorCode = SAVESTATUS_ERR_SAVE_CLOSE; + return false; } diff --git a/src/save/PCSave.cpp b/src/save/PCSave.cpp index 2702bd6e..e94db6db 100644 --- a/src/save/PCSave.cpp +++ b/src/save/PCSave.cpp @@ -38,7 +38,7 @@ C_PcSave::SaveSlot(int32 slot) if (file != 0) { DoGameSpecificStuffBeforeSave(); if (GenericSave(file)) { - if (CFileMgr::CloseFile(file) != 0) + if (!!CFileMgr::CloseFile(file)) nErrorCode = SAVESTATUS_ERR_SAVE_CLOSE; return true; } @@ -55,21 +55,21 @@ C_PcSave::PcClassSaveRoutine(int32 file, uint8 *data, uint32 size) CFileMgr::Write(file, (const char*)&size, sizeof(size)); if (CFileMgr::GetErrorReadWrite(file)) { nErrorCode = SAVESTATUS_ERR_SAVE_WRITE; - strncpy(SaveFileNameJustSaved, ValidSaveName, 259); + strncpy(SaveFileNameJustSaved, ValidSaveName, sizeof(ValidSaveName) - 1); return false; } CFileMgr::Write(file, (const char*)data, align4bytes(size)); - CheckSum += ((uint8*)&size)[0]; - CheckSum += ((uint8*)&size)[1]; - CheckSum += ((uint8*)&size)[2]; - CheckSum += ((uint8*)&size)[3]; + CheckSum += (uint8) size; + CheckSum += (uint8) (size >> 8); + CheckSum += (uint8) (size >> 16); + CheckSum += (uint8) (size >> 24); for (int i = 0; i < align4bytes(size); i++) { CheckSum += *data++; } if (CFileMgr::GetErrorReadWrite(file)) { nErrorCode = SAVESTATUS_ERR_SAVE_WRITE; - strncpy(SaveFileNameJustSaved, ValidSaveName, 259); + strncpy(SaveFileNameJustSaved, ValidSaveName, sizeof(ValidSaveName) - 1); return false; } From edc92a7bcc6a2ddf1e29815e4696d9be0420bae1 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:00:58 +0100 Subject: [PATCH 18/70] Update Fire.cpp --- src/core/Fire.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index 864c2509..ef9b08bd 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -410,7 +410,7 @@ CFireManager::StartScriptFire(const CVector &pos, CEntity *target, float strengt bool CFireManager::IsScriptFireExtinguish(int16 index) { - return (!m_aFires[index].m_bIsOngoing) ? true : false; + return m_aFires[index].m_bIsOngoing ? false : true; } void From 853b2d0c7890b5a0bb33617b33574086a0fe2b81 Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:05:01 +0100 Subject: [PATCH 19/70] Update Fire.cpp --- src/core/Fire.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index ef9b08bd..63442787 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -410,7 +410,7 @@ CFireManager::StartScriptFire(const CVector &pos, CEntity *target, float strengt bool CFireManager::IsScriptFireExtinguish(int16 index) { - return m_aFires[index].m_bIsOngoing ? false : true; + return !m_aFires[index].m_bIsOngoing; } void From 79822e3f7c1c0255f9b0dad2d874d88a1c2b2c3f Mon Sep 17 00:00:00 2001 From: blingu <36486731+blingu@users.noreply.github.com> Date: Thu, 26 Mar 2020 17:11:04 +0100 Subject: [PATCH 20/70] Update Fire.cpp --- src/core/Fire.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index 63442787..c98c808d 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -116,7 +116,7 @@ CFire::ProcessFire(void) rand(); rand(); rand(); /* unsure why these three rands are called */ CParticle::AddParticle(PARTICLE_CARFLAME_SMOKE, firePos, - CVector(0.0f, 0.0f, 0.0f), 0, 0.0, 0, 0, 0, 0); + CVector(0.0f, 0.0f, 0.0f), 0, 0.0f, 0, 0, 0, 0); } if (CTimer::GetTimeInMilliseconds() < m_nExtinguishTime || m_bIsScriptFire) { if (CTimer::GetTimeInMilliseconds() > m_nStartTime) @@ -129,14 +129,14 @@ CFire::ProcessFire(void) if (!m_pEntity) { CShadows::StoreStaticShadow((uint32)this, SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &lightpos, - 7.0, 0.0, 0.0, -7.0, 0, nRandNumber / 2, nRandNumber / 2, - 0, 10.0, 1.0, 40.0, 0, 0.0); + 7.0f, 0.0f, 0.0f, -7.0f, 0, nRandNumber / 2, nRandNumber / 2, + 0, 10.0f, 1.0f, 40.0f, 0, 0.0f); } fGreen = nRandNumber / 128; fRed = nRandNumber / 128; CPointLights::AddLight(0, m_vecPos, CVector(0.0f, 0.0f, 0.0f), - 12.0, fRed, fGreen, 0, 0, 0); + 12.0f, fRed, fGreen, 0, 0, 0); } else { Extinguish(); } @@ -312,7 +312,7 @@ CFire * CFireManager::FindFurthestFire_NeverMindFireMen(CVector coords, float minRange, float maxRange) { int furthestFire = -1; - float lastFireDist = 0.0; + float lastFireDist = 0.0f; float fireDist; for (int i = 0; i < NUM_FIRES; i++) { From f0dfaac838fdbb90783609bf4e45518ccf853708 Mon Sep 17 00:00:00 2001 From: aap Date: Thu, 26 Mar 2020 14:16:06 +0100 Subject: [PATCH 21/70] Finished CCam; various smaller things --- README.md | 1 - src/control/SceneEdit.cpp | 5 + src/control/SceneEdit.h | 5 + src/core/Cam.cpp | 4147 +++++++++++++++++++++++++++++++++++++ src/core/Camera.cpp | 1171 +---------- src/core/Camera.h | 102 +- src/core/Debug.cpp | 46 + src/core/Debug.h | 14 + src/core/Pad.h | 6 + src/core/World.cpp | 71 +- src/core/World.h | 6 +- src/core/config.h | 2 + src/core/main.cpp | 1 + src/core/re3.cpp | 35 +- src/peds/Ped.cpp | 4 +- src/render/Font.cpp | 2 +- src/render/Hud.cpp | 39 +- src/render/Renderer.cpp | 6 + 18 files changed, 4397 insertions(+), 1266 deletions(-) create mode 100644 src/core/Cam.cpp diff --git a/README.md b/README.md index 6e8f717c..3c394e62 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ CBoat CBrightLights CBulletInfo CBulletTraces -CCam CCamera CCopPed CCrane diff --git a/src/control/SceneEdit.cpp b/src/control/SceneEdit.cpp index 28b4ea6c..8dec3435 100644 --- a/src/control/SceneEdit.cpp +++ b/src/control/SceneEdit.cpp @@ -2,5 +2,10 @@ #include "patcher.h" #include "SceneEdit.h" +int &CSceneEdit::m_bCameraFollowActor = *(int*)0x940590; +bool &CSceneEdit::m_bRecording = *(bool*)0x95CD1F; +CVector &CSceneEdit::m_vecCurrentPosition = *(CVector*)0x943064; +CVector &CSceneEdit::m_vecCamHeading = *(CVector*)0x942F8C; + WRAPPER void CSceneEdit::Update(void) { EAXJMP(0x585570); } WRAPPER void CSceneEdit::Init(void) { EAXJMP(0x585170); } diff --git a/src/control/SceneEdit.h b/src/control/SceneEdit.h index e9209b90..efcdb022 100644 --- a/src/control/SceneEdit.h +++ b/src/control/SceneEdit.h @@ -3,6 +3,11 @@ class CSceneEdit { public: + static int &m_bCameraFollowActor; + static bool &m_bRecording; + static CVector &m_vecCurrentPosition; + static CVector &m_vecCamHeading; + static void Update(void); static void Init(void); }; diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp new file mode 100644 index 00000000..491a982c --- /dev/null +++ b/src/core/Cam.cpp @@ -0,0 +1,4147 @@ +#include "common.h" +#include "patcher.h" +#include "main.h" +#include "Draw.h" +#include "World.h" +#include "Vehicle.h" +#include "Automobile.h" +#include "Ped.h" +#include "PlayerPed.h" +#include "CopPed.h" +#include "RpAnimBlend.h" +#include "ControllerConfig.h" +#include "Pad.h" +#include "Frontend.h" +#include "General.h" +#include "Renderer.h" +#include "Shadows.h" +#include "Hud.h" +#include "ZoneCull.h" +#include "SurfaceTable.h" +#include "WaterLevel.h" +#include "MBlur.h" +#include "SceneEdit.h" +#include "Debug.h" +#include "Camera.h" + +const float DefaultFOV = 70.0f; // beta: 80.0f + +bool PrintDebugCode = false; +int16 &DebugCamMode = *(int16*)0x95CCF2; + +void +CCam::Init(void) +{ + Mode = MODE_FOLLOWPED; + Front = CVector(0.0f, 0.0f, -1.0f); + Up = CVector(0.0f, 0.0f, 1.0f); + Rotating = 0; + m_iDoCollisionChecksOnFrameNum = 1; + m_iDoCollisionCheckEveryNumOfFrames = 9; + m_iFrameNumWereAt = 0; + m_bCollisionChecksOn = 1; + m_fRealGroundDist = 0.0f; + BetaSpeed = 0.0f; + AlphaSpeed = 0.0f; + DistanceSpeed = 0.0f; + f_max_role_angle = DEGTORAD(5.0f); + Distance = 30.0f; + DistanceSpeed = 0.0f; + m_pLastCarEntered = 0; + m_pLastPedLookedAt = 0; + ResetStatics = 1; + Beta = 0.0f; + m_bFixingBeta = 0; + CA_MIN_DISTANCE = 0.0f; + CA_MAX_DISTANCE = 0.0f; + LookingBehind = 0; + LookingLeft = 0; + LookingRight = 0; + m_fPlayerInFrontSyphonAngleOffSet = DEGTORAD(20.0f); + m_fSyphonModeTargetZOffSet = 0.5f; + m_fRadiusForDead = 1.5f; + DirectionWasLooking = LOOKING_FORWARD; + LookBehindCamWasInFront = 0; + f_Roll = 0.0f; + f_rollSpeed = 0.0f; + m_fCloseInPedHeightOffset = 0.0f; + m_fCloseInPedHeightOffsetSpeed = 0.0f; + m_fCloseInCarHeightOffset = 0.0f; + m_fCloseInCarHeightOffsetSpeed = 0.0f; + m_fPedBetweenCameraHeightOffset = 0.0f; + m_fTargetBeta = 0.0f; + m_fBufferedTargetBeta = 0.0f; + m_fBufferedTargetOrientation = 0.0f; + m_fBufferedTargetOrientationSpeed = 0.0f; + m_fDimensionOfHighestNearCar = 0.0f; + m_fRoadOffSet = 0.0f; +} + +void +CCam::Process(void) +{ + CVector CameraTarget; + float TargetSpeedVar = 0.0f; + float TargetOrientation = 0.0f; + + if(CamTargetEntity == nil) + CamTargetEntity = TheCamera.pTargetEntity; + + m_iFrameNumWereAt++; + if(m_iFrameNumWereAt > m_iDoCollisionCheckEveryNumOfFrames) + m_iFrameNumWereAt = 1; + m_bCollisionChecksOn = m_iFrameNumWereAt == m_iDoCollisionChecksOnFrameNum; + + if(m_bCamLookingAtVector){ + CameraTarget = m_cvecCamFixedModeVector; + }else if(CamTargetEntity->IsVehicle()){ + CameraTarget = CamTargetEntity->GetPosition(); + + if(CamTargetEntity->GetForward().x == 0.0f && CamTargetEntity->GetForward().y == 0.0f) + TargetOrientation = 0.0f; + else + TargetOrientation = CGeneral::GetATanOfXY(CamTargetEntity->GetForward().x, CamTargetEntity->GetForward().y); + + CVector Fwd(0.0f, 0.0f, 0.0f); + Fwd.x = CamTargetEntity->GetForward().x; + Fwd.y = CamTargetEntity->GetForward().y; + Fwd.Normalise(); + // Game normalizes again here manually. useless, so skipped + + float FwdSpeedX = ((CVehicle*)CamTargetEntity)->GetMoveSpeed().x * Fwd.x; + float FwdSpeedY = ((CVehicle*)CamTargetEntity)->GetMoveSpeed().y * Fwd.y; + if(FwdSpeedX + FwdSpeedY > 0.0f) + TargetSpeedVar = min(Sqrt(SQR(FwdSpeedX) + SQR(FwdSpeedY))/0.9f, 1.0f); + else + TargetSpeedVar = -min(Sqrt(SQR(FwdSpeedX) + SQR(FwdSpeedY))/1.8f, 0.5f); + SpeedVar = 0.895f*SpeedVar + 0.105*TargetSpeedVar; + }else{ + CameraTarget = CamTargetEntity->GetPosition(); + + if(CamTargetEntity->GetForward().x == 0.0f && CamTargetEntity->GetForward().y == 0.0f) + TargetOrientation = 0.0f; + else + TargetOrientation = CGeneral::GetATanOfXY(CamTargetEntity->GetForward().x, CamTargetEntity->GetForward().y); + TargetSpeedVar = 0.0f; + SpeedVar = 0.0f; + } + + switch(Mode){ + case MODE_TOPDOWN: + case MODE_GTACLASSIC: + Process_TopDown(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_BEHINDCAR: + Process_BehindCar(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_FOLLOWPED: + if(CCamera::m_bUseMouse3rdPerson) + Process_FollowPedWithMouse(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + else + Process_FollowPed(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; +// case MODE_AIMING: + case MODE_DEBUG: + Process_Debug(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_SNIPER: + Process_Sniper(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_ROCKETLAUNCHER: + Process_Rocket(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_MODELVIEW: + Process_ModelView(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; +// case MODE_BILL: + case MODE_SYPHON: + Process_Syphon(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_CIRCLE: + Process_Circle(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; +// case MODE_CHEESYZOOM: + case MODE_WHEELCAM: + Process_WheelCam(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_FIXED: + Process_Fixed(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_1STPERSON: + Process_1stPerson(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_FLYBY: + Process_FlyBy(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_CAM_ON_A_STRING: + Process_Cam_On_A_String(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; +// case MODE_REACTION: +// case MODE_FOLLOW_PED_WITH_BIND: +// case MODE_CHRIS: + case MODE_BEHINDBOAT: + Process_BehindBoat(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_PLAYER_FALLEN_WATER: + Process_Player_Fallen_Water(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; +// case MODE_CAM_ON_TRAIN_ROOF: +// case MODE_CAM_RUNNING_SIDE_TRAIN: +// case MODE_BLOOD_ON_THE_TRACKS: +// case MODE_IM_THE_PASSENGER_WOOWOO: + case MODE_SYPHON_CRIM_IN_FRONT: + Process_Syphon_Crim_In_Front(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_PED_DEAD_BABY: + ProcessPedsDeadBaby(); + break; +// case MODE_PILLOWS_PAPS: +// case MODE_LOOK_AT_CARS: + case MODE_ARRESTCAM_ONE: + ProcessArrestCamOne(); + break; + case MODE_ARRESTCAM_TWO: + ProcessArrestCamTwo(); + break; + case MODE_M16_1STPERSON: + case MODE_HELICANNON_1STPERSON: // miami + Process_M16_1stPerson(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_SPECIAL_FIXED_FOR_SYPHON: + Process_SpecialFixedForSyphon(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_FIGHT_CAM: + Process_Fight_Cam(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_TOP_DOWN_PED: + Process_TopDownPed(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_SNIPER_RUNABOUT: + case MODE_ROCKETLAUNCHER_RUNABOUT: + case MODE_1STPERSON_RUNABOUT: + case MODE_M16_1STPERSON_RUNABOUT: + case MODE_FIGHT_CAM_RUNABOUT: + Process_1rstPersonPedOnPC(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_EDITOR: + Process_Editor(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + default: + Source = CVector(0.0f, 0.0f, 0.0f); + Front = CVector(0.0f, 1.0f, 0.0f); + Up = CVector(0.0f, 0.0f, 1.0f); + } + + CVector TargetToCam = Source - m_cvecTargetCoorsForFudgeInter; + float DistOnGround = TargetToCam.Magnitude2D(); + m_fTrueBeta = CGeneral::GetATanOfXY(TargetToCam.x, TargetToCam.y); + m_fTrueAlpha = CGeneral::GetATanOfXY(TargetToCam.z, DistOnGround); + if(TheCamera.m_uiTransitionState == 0) // TODO? what values are possible? enum? + KeepTrackOfTheSpeed(Source, m_cvecTargetCoorsForFudgeInter, Up, m_fTrueAlpha, m_fTrueBeta, FOV); + + // Look Behind, Left, Right + LookingBehind = false; + LookingLeft = false; + LookingRight = false; + SourceBeforeLookBehind = Source; + if(&TheCamera.Cams[TheCamera.ActiveCam] == this){ + if((Mode == MODE_CAM_ON_A_STRING || Mode == MODE_1STPERSON || Mode == MODE_BEHINDBOAT) && + CamTargetEntity->IsVehicle()){ + if(CPad::GetPad(0)->GetLookBehindForCar()){ + LookBehind(); + if(DirectionWasLooking != LOOKING_BEHIND) + TheCamera.m_bJust_Switched = true; + DirectionWasLooking = LOOKING_BEHIND; + }else if(CPad::GetPad(0)->GetLookLeft()){ + LookLeft(); + if(DirectionWasLooking != LOOKING_LEFT) + TheCamera.m_bJust_Switched = true; + DirectionWasLooking = LOOKING_LEFT; + }else if(CPad::GetPad(0)->GetLookRight()){ + LookRight(); + if(DirectionWasLooking != LOOKING_RIGHT) + TheCamera.m_bJust_Switched = true; + DirectionWasLooking = LOOKING_RIGHT; + }else{ + if(DirectionWasLooking != LOOKING_FORWARD) + TheCamera.m_bJust_Switched = true; + DirectionWasLooking = LOOKING_FORWARD; + } + } + if(Mode == MODE_FOLLOWPED && CamTargetEntity->IsPed()){ + if(CPad::GetPad(0)->GetLookBehindForPed()){ + LookBehind(); + if(DirectionWasLooking != LOOKING_BEHIND) + TheCamera.m_bJust_Switched = true; + DirectionWasLooking = LOOKING_BEHIND; + }else + DirectionWasLooking = LOOKING_FORWARD; + } + } + + if(Mode == MODE_SNIPER || Mode == MODE_ROCKETLAUNCHER || Mode == MODE_M16_1STPERSON || + Mode == MODE_1STPERSON || Mode == MODE_HELICANNON_1STPERSON || GetWeaponFirstPersonOn()) + ClipIfPedInFrontOfPlayer(); +} + +// MaxSpeed is a limit of how fast the value is allowed to change. 1.0 = to Target in up to 1ms +// Acceleration is how fast the speed will change to MaxSpeed. 1.0 = to MaxSpeed in 1ms +void +WellBufferMe(float Target, float *CurrentValue, float *CurrentSpeed, float MaxSpeed, float Acceleration, bool IsAngle) +{ + float Delta = Target - *CurrentValue; + + if(IsAngle){ + while(Delta >= PI) Delta -= 2*PI; + while(Delta < -PI) Delta += 2*PI; + } + + float TargetSpeed = Delta * MaxSpeed; + // Add or subtract absolute depending on sign, genius! +// if(TargetSpeed - *CurrentSpeed > 0.0f) +// *CurrentSpeed += Acceleration * Abs(TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); +// else +// *CurrentSpeed -= Acceleration * Abs(TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); + // this is simpler: + *CurrentSpeed += Acceleration * (TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); + + // Clamp speed if we overshot + if(TargetSpeed < 0.0f && *CurrentSpeed < TargetSpeed) + *CurrentSpeed = TargetSpeed; + else if(TargetSpeed > 0.0f && *CurrentSpeed > TargetSpeed) + *CurrentSpeed = TargetSpeed; + + *CurrentValue += *CurrentSpeed * min(10.0f, CTimer::GetTimeStep()); +} + +void +MakeAngleLessThan180(float &Angle) +{ + while(Angle >= PI) Angle -= 2*PI; + while(Angle < -PI) Angle += 2*PI; +} + +void +CCam::ProcessSpecialHeightRoutines(void) +{ + int i = 0; + bool StandingOnBoat = false; + static bool PreviouslyFailedRoadHeightCheck = false; + CVector CamToTarget, CamToPed; + float DistOnGround, BetaAngle; + CPed *Player; + int ClosestPed = 0; + bool FoundPed = false; + float ClosestPedDist, PedZDist; + CColPoint colPoint; + + CamToTarget = TheCamera.pTargetEntity->GetPosition() - TheCamera.GetGameCamPosition(); + DistOnGround = CamToTarget.Magnitude2D(); + BetaAngle = CGeneral::GetATanOfXY(CamToTarget.x, CamToTarget.y); + m_bTheHeightFixerVehicleIsATrain = false; + ClosestPedDist = 0.0f; + // CGeneral::GetATanOfXY(TheCamera.GetForward().x, TheCamera.GetForward().y); + Player = CWorld::Players[CWorld::PlayerInFocus].m_pPed; + + if(DistOnGround > 10.0f) + DistOnGround = 10.0f; + + if(CamTargetEntity && CamTargetEntity->IsPed()){ + if(FindPlayerPed()->m_pCurSurface && FindPlayerPed()->m_pCurSurface->IsVehicle() && + ((CVehicle*)FindPlayerPed()->m_pCurSurface)->IsBoat()) + StandingOnBoat = true; + + // Move up the camera if there is a ped close to it + if(Mode == MODE_FOLLOWPED || Mode == MODE_FIGHT_CAM){ + // Find ped closest to camera + while(i < Player->m_numNearPeds){ + if(Player->m_nearPeds[i] && Player->m_nearPeds[i]->GetPedState() != PED_DEAD){ + CamToPed = Player->m_nearPeds[i]->GetPosition() - TheCamera.GetGameCamPosition(); + if(FoundPed){ + if(CamToPed.Magnitude2D() < ClosestPedDist){ + ClosestPed = i; + ClosestPedDist = CamToPed.Magnitude2D(); + } + }else{ + FoundPed = true; + ClosestPed = i; + ClosestPedDist = CamToPed.Magnitude2D(); + } + } + i++; + } + + if(FoundPed){ + float Offset = 0.0f; + CPed *Ped = Player->m_nearPeds[ClosestPed]; + CamToPed = Ped->GetPosition() - TheCamera.GetGameCamPosition(); + PedZDist = 0.0f; + float dist = CamToPed.Magnitude2D(); // should be same as ClosestPedDist + if(dist < 2.1f){ + // Ped is close to camera, move up + + // Z Distance between player and close ped + PedZDist = 0.0f; + if(Ped->bIsStanding) + PedZDist = Ped->GetPosition().z - Player->GetPosition().z; + // Ignore if too distant + if(PedZDist > 1.2f || PedZDist < -1.2f) + PedZDist = 0.0f; + + float DistScale = (2.1f - dist)/2.1f; + if(Mode == MODE_FOLLOWPED){ + if(TheCamera.PedZoomIndicator == 1.0f) + Offset = 0.45*DistScale + PedZDist; + if(TheCamera.PedZoomIndicator == 2.0f) + Offset = 0.35*DistScale + PedZDist; + if(TheCamera.PedZoomIndicator == 3.0f) + Offset = 0.25*DistScale + PedZDist; + if(Abs(CGeneral::GetRadianAngleBetweenPoints(CamToPed.x, CamToPed.y, CamToTarget.x, CamToTarget.y)) > HALFPI) + Offset += 0.3f; + m_fPedBetweenCameraHeightOffset = Offset + 1.3f; + PedZDist = 0.0f; + }else if(Mode == MODE_FIGHT_CAM) + m_fPedBetweenCameraHeightOffset = PedZDist + 1.3f + 0.5f; + }else + m_fPedBetweenCameraHeightOffset = 0.0f; + }else{ + PedZDist = 0.0f; + m_fPedBetweenCameraHeightOffset = 0.0f; + } + }else + PedZDist = 0.0f; + + + // Move camera up for vehicles in the way + if(m_bCollisionChecksOn && (Mode == MODE_FOLLOWPED || Mode == MODE_FIGHT_CAM)){ + bool FoundCar = false; + CEntity *vehicle = nil; + float TestDist = DistOnGround + 1.25f; + float HighestCar = 0.0f; + CVector TestBase = CamTargetEntity->GetPosition(); + CVector TestPoint; + TestBase.z -= 0.15f; + + TestPoint = TestBase - TestDist * CVector(Cos(BetaAngle), Sin(BetaAngle), 0.0f); + if(CWorld::ProcessLineOfSight(CamTargetEntity->GetPosition(), TestPoint, colPoint, vehicle, false, true, false, false, false, false) && + vehicle->IsVehicle()){ + float height = vehicle->GetColModel()->boundingBox.GetSize().z; + FoundCar = true; + HighestCar = height; + if(((CVehicle*)vehicle)->IsTrain()) + m_bTheHeightFixerVehicleIsATrain = true; + } + + TestPoint = TestBase - TestDist * CVector(Cos(BetaAngle+DEGTORAD(28.0f)), Sin(BetaAngle+DEGTORAD(28.0f)), 0.0f); + if(CWorld::ProcessLineOfSight(CamTargetEntity->GetPosition(), TestPoint, colPoint, vehicle, false, true, false, false, false, false) && + vehicle->IsVehicle()){ + float height = vehicle->GetColModel()->boundingBox.GetSize().z; + if(FoundCar){ + HighestCar = max(HighestCar, height); + }else{ + FoundCar = true; + HighestCar = height; + } + if(((CVehicle*)vehicle)->IsTrain()) + m_bTheHeightFixerVehicleIsATrain = true; + } + + TestPoint = TestBase - TestDist * CVector(Cos(BetaAngle-DEGTORAD(28.0f)), Sin(BetaAngle-DEGTORAD(28.0f)), 0.0f); + if(CWorld::ProcessLineOfSight(CamTargetEntity->GetPosition(), TestPoint, colPoint, vehicle, false, true, false, false, false, false) && + vehicle->IsVehicle()){ + float height = vehicle->GetColModel()->boundingBox.GetSize().z; + if(FoundCar){ + HighestCar = max(HighestCar, height); + }else{ + FoundCar = true; + HighestCar = height; + } + if(((CVehicle*)vehicle)->IsTrain()) + m_bTheHeightFixerVehicleIsATrain = true; + } + + if(FoundCar){ + m_fDimensionOfHighestNearCar = HighestCar + 0.1f; + if(Mode == MODE_FIGHT_CAM) + m_fDimensionOfHighestNearCar += 0.75f; + }else + m_fDimensionOfHighestNearCar = 0.0f; + } + + // Move up for road + if(Mode == MODE_FOLLOWPED || Mode == MODE_FIGHT_CAM || + Mode == MODE_SYPHON || Mode == MODE_SYPHON_CRIM_IN_FRONT || Mode == MODE_SPECIAL_FIXED_FOR_SYPHON){ + bool Inside = false; + bool OnRoad = false; + + switch(((CPhysical*)CamTargetEntity)->m_nSurfaceTouched) + case SURFACE_GRASS: + case SURFACE_DIRT: + case SURFACE_DIRTTRACK: + case SURFACE_STEEL: + case SURFACE_TIRE: + case SURFACE_STONE: + OnRoad = true; + + if(CCullZones::PlayerNoRain()) + Inside = true; + + if((m_bCollisionChecksOn || PreviouslyFailedRoadHeightCheck || OnRoad) && + m_fCloseInPedHeightOffset < 0.0001f && !Inside){ + CVector TestPoint; + CEntity *road; + float GroundZ = 0.0f; + bool FoundGround = false; + float RoofZ = 0.0f; + bool FoundRoof = false; + static float MinHeightAboveRoad = 0.9f; + + TestPoint = CamTargetEntity->GetPosition() - DistOnGround * CVector(Cos(BetaAngle), Sin(BetaAngle), 0.0f); + m_fRoadOffSet = 0.0f; + + if(CWorld::ProcessVerticalLine(TestPoint, -1000.0f, colPoint, road, true, false, false, false, false, false, nil)){ + FoundGround = true; + GroundZ = colPoint.point.z; + } + // Move up if too close to ground + if(FoundGround){ + if(TestPoint.z - GroundZ < MinHeightAboveRoad){ + m_fRoadOffSet = GroundZ + MinHeightAboveRoad - TestPoint.z; + PreviouslyFailedRoadHeightCheck = true; + }else{ + if(m_bCollisionChecksOn) + PreviouslyFailedRoadHeightCheck = false; + else + m_fRoadOffSet = 0.0f; + } + }else{ + if(CWorld::ProcessVerticalLine(TestPoint, 1000.0f, colPoint, road, true, false, false, false, false, false, nil)){ + FoundRoof = true; + RoofZ = colPoint.point.z; + } + if(FoundRoof){ + if(TestPoint.z - RoofZ < MinHeightAboveRoad){ + m_fRoadOffSet = RoofZ + MinHeightAboveRoad - TestPoint.z; + PreviouslyFailedRoadHeightCheck = true; + }else{ + if(m_bCollisionChecksOn) + PreviouslyFailedRoadHeightCheck = false; + else + m_fRoadOffSet = 0.0f; + } + } + } + } + } + + if(PreviouslyFailedRoadHeightCheck && m_fCloseInPedHeightOffset < 0.0001f){ + if(colPoint.surfaceB != SURFACE_TARMAC && + colPoint.surfaceB != SURFACE_GRASS && + colPoint.surfaceB != SURFACE_DIRT && + colPoint.surfaceB != SURFACE_DIRTTRACK && + colPoint.surfaceB != SURFACE_STONE){ + if(m_fRoadOffSet > 1.4f) + m_fRoadOffSet = 1.4f; + }else{ + if(Mode == MODE_FOLLOWPED){ + if(TheCamera.PedZoomIndicator == 1.0f) + m_fRoadOffSet += 0.2f; + if(TheCamera.PedZoomIndicator == 2.0f) + m_fRoadOffSet += 0.5f; + if(TheCamera.PedZoomIndicator == 3.0f) + m_fRoadOffSet += 0.95f; + } + } + } + } + + if(StandingOnBoat){ + m_fRoadOffSet = 0.0f; + m_fDimensionOfHighestNearCar = 1.0f; + m_fPedBetweenCameraHeightOffset = 0.0f; + } +} + +void +CCam::GetVectorsReadyForRW(void) +{ + CVector right; + Up = CVector(0.0f, 0.0f, 1.0f); + Front.Normalise(); + if(Front.x == 0.0f && Front.y == 0.0f){ + Front.x = 0.0001f; + Front.y = 0.0001f; + } + right = CrossProduct(Front, Up); + right.Normalise(); + Up = CrossProduct(right, Front); +} + +void +CCam::LookBehind(void) +{ + float Dist, DeltaBeta, TargetOrientation, Angle; + CVector TargetCoors, TargetFwd, TestCoors; + CColPoint colPoint; + CEntity *entity; + + TargetCoors = CamTargetEntity->GetPosition(); + Front = CamTargetEntity->GetPosition() - Source; + + if((Mode == MODE_CAM_ON_A_STRING || Mode == MODE_BEHINDBOAT) && CamTargetEntity->IsVehicle()){ + LookingBehind = true; + Dist = Mode == MODE_CAM_ON_A_STRING ? CA_MAX_DISTANCE : 15.5f; + TargetFwd = CamTargetEntity->GetForward(); + TargetFwd.Normalise(); + TargetOrientation = CGeneral::GetATanOfXY(TargetFwd.x, TargetFwd.y); + DeltaBeta = TargetOrientation - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(DirectionWasLooking == LOOKING_BEHIND) + LookBehindCamWasInFront = DeltaBeta <= -HALFPI || DeltaBeta >= HALFPI; + if(LookBehindCamWasInFront) + TargetOrientation += PI; + Source.x = Dist*Cos(TargetOrientation) + TargetCoors.x; + Source.y = Dist*Sin(TargetOrientation) + TargetCoors.y; + Source.z -= 1.0f; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + Source = colPoint.point; + } + Source.z += 1.0f; + Front = CamTargetEntity->GetPosition() - Source; + GetVectorsReadyForRW(); + } + if(Mode == MODE_1STPERSON && CamTargetEntity->IsVehicle()){ + LookingBehind = true; + RwCameraSetNearClipPlane(Scene.camera, 0.25f); + Front = CamTargetEntity->GetForward(); + Front.Normalise(); + if(((CVehicle*)CamTargetEntity)->IsBoat()) + Source.z -= 0.5f; + Source += 0.25f*Front; + Front = -Front; +#ifdef FIX_BUGS + // not sure if this is a bug... + GetVectorsReadyForRW(); +#endif + } + if(CamTargetEntity->IsPed()){ + Angle = CGeneral::GetATanOfXY(Source.x - TargetCoors.x, Source.y - TargetCoors.y) + PI; + Source.x = 4.5f*Cos(Angle) + TargetCoors.x; + Source.y = 4.5f*Sin(Angle) + TargetCoors.y; + Source.z = 1.15f + TargetCoors.z; + TestCoors = TargetCoors; + TestCoors.z = Source.z; + if(CWorld::ProcessLineOfSight(TestCoors, Source, colPoint, entity, true, true, false, true, false, true, true)){ + Source.x = colPoint.point.x; + Source.y = colPoint.point.y; + if((TargetCoors - Source).Magnitude2D() < 1.15f) + RwCameraSetNearClipPlane(Scene.camera, 0.05f); + } + Front = TargetCoors - Source; + GetVectorsReadyForRW(); + } +} + +void +CCam::LookLeft(void) +{ + float Dist, TargetOrientation; + CVector TargetCoors, TargetFwd; + CColPoint colPoint; + CEntity *entity; + + if((Mode == MODE_CAM_ON_A_STRING || Mode == MODE_BEHINDBOAT) && CamTargetEntity->IsVehicle()){ + LookingLeft = true; + TargetCoors = CamTargetEntity->GetPosition(); + Front = CamTargetEntity->GetPosition() - Source; + Dist = Mode == MODE_CAM_ON_A_STRING ? CA_MAX_DISTANCE : 9.0f; + TargetFwd = CamTargetEntity->GetForward(); + TargetFwd.Normalise(); + TargetOrientation = CGeneral::GetATanOfXY(TargetFwd.x, TargetFwd.y); + Source.x = Dist*Cos(TargetOrientation - HALFPI) + TargetCoors.x; + Source.y = Dist*Sin(TargetOrientation - HALFPI) + TargetCoors.y; + Source.z -= 1.0f; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source = colPoint.point; + } + Source.z += 1.0f; + Front = CamTargetEntity->GetPosition() - Source; + Front.z += 1.1f; + if(Mode == MODE_BEHINDBOAT) + Front.z += 1.2f; + GetVectorsReadyForRW(); + } + if(Mode == MODE_1STPERSON && CamTargetEntity->IsVehicle()){ + LookingLeft = true; + RwCameraSetNearClipPlane(Scene.camera, 0.25f); + if(((CVehicle*)CamTargetEntity)->IsBoat()) + Source.z -= 0.5f; + + Up = CamTargetEntity->GetUp(); + Up.Normalise(); + Front = CamTargetEntity->GetForward(); + Front.Normalise(); + Front = -CrossProduct(Front, Up); + Front.Normalise(); +#ifdef FIX_BUGS + // not sure if this is a bug... + GetVectorsReadyForRW(); +#endif + } +} + +void +CCam::LookRight(void) +{ + float Dist, TargetOrientation; + CVector TargetCoors, TargetFwd; + CColPoint colPoint; + CEntity *entity; + + if((Mode == MODE_CAM_ON_A_STRING || Mode == MODE_BEHINDBOAT) && CamTargetEntity->IsVehicle()){ + LookingRight = true; + TargetCoors = CamTargetEntity->GetPosition(); + Front = CamTargetEntity->GetPosition() - Source; + Dist = Mode == MODE_CAM_ON_A_STRING ? CA_MAX_DISTANCE : 9.0f; + TargetFwd = CamTargetEntity->GetForward(); + TargetFwd.Normalise(); + TargetOrientation = CGeneral::GetATanOfXY(TargetFwd.x, TargetFwd.y); + Source.x = Dist*Cos(TargetOrientation + HALFPI) + TargetCoors.x; + Source.y = Dist*Sin(TargetOrientation + HALFPI) + TargetCoors.y; + Source.z -= 1.0f; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source = colPoint.point; + } + Source.z += 1.0f; + Front = CamTargetEntity->GetPosition() - Source; + Front.z += 1.1f; + if(Mode == MODE_BEHINDBOAT) + Front.z += 1.2f; + GetVectorsReadyForRW(); + } + if(Mode == MODE_1STPERSON && CamTargetEntity->IsVehicle()){ + LookingRight = true; + RwCameraSetNearClipPlane(Scene.camera, 0.25f); + if(((CVehicle*)CamTargetEntity)->IsBoat()) + Source.z -= 0.5f; + + Up = CamTargetEntity->GetUp(); + Up.Normalise(); + Front = CamTargetEntity->GetForward(); + Front.Normalise(); + Front = CrossProduct(Front, Up); + Front.Normalise(); +#ifdef FIX_BUGS + // not sure if this is a bug... + GetVectorsReadyForRW(); +#endif + } +} + +void +CCam::ClipIfPedInFrontOfPlayer(void) +{ + float FwdAngle, PedAngle, DeltaAngle, fDist, Near; + CVector vDist; + CPed *Player; + bool found = false; + int ped = 0; + + // unused: TheCamera.pTargetEntity->GetPosition() - TheCamera.GetGameCamPosition(); + + FwdAngle = CGeneral::GetATanOfXY(TheCamera.GetForward().x, TheCamera.GetForward().y); + Player = CWorld::Players[CWorld::PlayerInFocus].m_pPed; + while(ped < Player->m_numNearPeds && !found) + if(Player->m_nearPeds[ped] && Player->m_nearPeds[ped]->GetPedState() != PED_DEAD) + found = true; + else + ped++; + if(found){ + vDist = Player->m_nearPeds[ped]->GetPosition() - TheCamera.GetGameCamPosition(); + PedAngle = CGeneral::GetATanOfXY(vDist.x, vDist.y); + DeltaAngle = FwdAngle - PedAngle; + while(DeltaAngle >= PI) DeltaAngle -= 2*PI; + while(DeltaAngle < -PI) DeltaAngle += 2*PI; + if(Abs(DeltaAngle) < HALFPI){ + fDist = Sqrt(SQR(vDist.x) + SQR(vDist.y)); + if(fDist < 1.25f){ + Near = 0.9f - (1.25f - fDist); + if(Near < 0.05f) + Near = 0.05f; + RwCameraSetNearClipPlane(Scene.camera, Near); + } + } + } +} + +void +CCam::KeepTrackOfTheSpeed(const CVector &source, const CVector &target, const CVector &up, const float &alpha, const float &beta, const float &fov) +{ + static CVector PreviousSource = source; + static CVector PreviousTarget = target; + static CVector PreviousUp = up; + static float PreviousBeta = beta; + static float PreviousAlpha = alpha; + static float PreviousFov = fov; + + if(TheCamera.m_bJust_Switched){ + PreviousSource = source; + PreviousTarget = target; + PreviousUp = up; + } + + m_cvecSourceSpeedOverOneFrame = PreviousSource - source; + m_cvecTargetSpeedOverOneFrame = PreviousTarget - target; + m_cvecUpOverOneFrame = PreviousUp - up; + m_fFovSpeedOverOneFrame = fov - PreviousFov; + m_fBetaSpeedOverOneFrame = beta - PreviousBeta; + MakeAngleLessThan180(m_fBetaSpeedOverOneFrame); + m_fAlphaSpeedOverOneFrame = alpha - PreviousAlpha; + MakeAngleLessThan180(m_fAlphaSpeedOverOneFrame); + + PreviousSource = source; + PreviousTarget = target; + PreviousUp = up; + PreviousBeta = beta; + PreviousAlpha = alpha; + PreviousFov = fov; +} + +bool +CCam::Using3rdPersonMouseCam(void) +{ + return CCamera::m_bUseMouse3rdPerson && + (Mode == MODE_FOLLOWPED || + TheCamera.m_bPlayerIsInGarage && + FindPlayerPed() && FindPlayerPed()->m_nPedState != PED_DRIVING && + Mode != MODE_TOPDOWN && this->CamTargetEntity == FindPlayerPed()); +} + +bool +CCam::GetWeaponFirstPersonOn(void) +{ + CEntity *target = this->CamTargetEntity; + if (target && target->IsPed()) + return ((CPed*)target)->GetWeapon()->m_bAddRotOffset; + + return false; +} + +bool +CCam::IsTargetInWater(const CVector &CamCoors) +{ + if(CamTargetEntity == nil) + return false; + if(CamTargetEntity->IsPed()){ + if(!((CPed*)CamTargetEntity)->bIsInWater) + return false; + if(!((CPed*)CamTargetEntity)->bIsStanding) + return true; + return false; + } + return ((CPhysical*)CamTargetEntity)->bIsInWater; +} + +void +CCam::PrintMode(void) +{ + // Doesn't do anything + char buf[256]; + + if(PrintDebugCode){ + sprintf(buf, " "); + sprintf(buf, " "); + sprintf(buf, " "); + + static char *modes[] = { "None", + "Top Down", "GTA Classic", "Behind Car", "Follow Ped", + "Aiming", "Debug", "Sniper", "Rocket", "Model Viewer", "Bill", + "Syphon", "Circle", "Cheesy Zoom", "Wheel", "Fixed", + "1st Person", "Fly by", "on a String", "Reaction", + "Follow Ped with Bind", "Chris", "Behind Boat", + "Player fallen in Water", "Train Roof", "Train Side", + "Blood on the tracks", "Passenger", "Syphon Crim in Front", + "Dead Baby", "Pillow Paps", "Look at Cars", "Arrest One", + "Arrest Two", "M16", "Special fixed for Syphon", "Fight", + "Top Down Ped", + "Sniper run about", "Rocket run about", + "1st Person run about", "M16 run about", "Fight run about", + "Editor" + }; + sprintf(buf, "Cam: %s", modes[TheCamera.Cams[TheCamera.ActiveCam].Mode]); + CDebug::PrintAt(buf, 2, 5); + } + + if(DebugCamMode != MODE_NONE){ + switch(Mode){ + case MODE_FOLLOWPED: + sprintf(buf, "Debug:- Cam Choice1. No Locking, used as game default"); + break; + case MODE_REACTION: + sprintf(buf, "Debug:- Cam Choice2. Reaction Cam On A String "); + sprintf(buf, " Uses Locking Button LeftShoulder 1. "); + break; + case MODE_FOLLOW_PED_WITH_BIND: + sprintf(buf, "Debug:- Cam Choice3. Game ReactionCam with Locking "); + sprintf(buf, " Uses Locking Button LeftShoulder 1. "); + break; + case MODE_CHRIS: + sprintf(buf, "Debug:- Cam Choice4. Chris's idea. "); + sprintf(buf, " Uses Locking Button LeftShoulder 1. "); + sprintf(buf, " Also control the camera using the right analogue stick."); + break; + } + } +} + +// This code is really bad. wtf R*? +CVector +CCam::DoAverageOnVector(const CVector &vec) +{ + int i; + CVector Average = { 0.0f, 0.0f, 0.0f }; + + if(ResetStatics){ + m_iRunningVectorArrayPos = 0; + m_iRunningVectorCounter = 1; + } + + // TODO: make this work with NUMBER_OF_VECTORS_FOR_AVERAGE != 2 + if(m_iRunningVectorCounter == 3){ + m_arrPreviousVectors[0] = m_arrPreviousVectors[1]; + m_arrPreviousVectors[1] = vec; + }else + m_arrPreviousVectors[m_iRunningVectorArrayPos] = vec; + + for(i = 0; i <= m_iRunningVectorArrayPos; i++) + Average += m_arrPreviousVectors[i]; + Average /= i; + + m_iRunningVectorArrayPos++; + m_iRunningVectorCounter++; + if(m_iRunningVectorArrayPos >= NUMBER_OF_VECTORS_FOR_AVERAGE) + m_iRunningVectorArrayPos = NUMBER_OF_VECTORS_FOR_AVERAGE-1; + if(m_iRunningVectorCounter > NUMBER_OF_VECTORS_FOR_AVERAGE+1) + m_iRunningVectorCounter = NUMBER_OF_VECTORS_FOR_AVERAGE+1; + + return Average; +} + +// Rotate Beta in direction opposite of BetaOffset in 5 deg. steps. +// Return the first angle for which Beta + BetaOffset + Angle has a clear view. +// i.e. BetaOffset is a safe zone so that Beta + Angle is really clear. +// If BetaOffset == 0, try both directions. +float +CCam::GetPedBetaAngleForClearView(const CVector &Target, float Dist, float BetaOffset, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies) +{ + CColPoint point; + CEntity *ent = nil; + CVector ToSource; + float a; + + // This would be so much nicer if we just got the step variable before the loop...R* + + for(a = 0.0f; a <= PI; a += DEGTORAD(5.0f)){ + if(BetaOffset <= 0.0f){ + ToSource = CVector(Cos(Beta + BetaOffset + a), Sin(Beta + BetaOffset + a), 0.0f)*Dist; + if(!CWorld::ProcessLineOfSight(Target, Target + ToSource, + point, ent, checkBuildings, checkVehicles, checkPeds, + checkObjects, checkDummies, true, true)) + return a; + } + if(BetaOffset >= 0.0f){ + ToSource = CVector(Cos(Beta + BetaOffset - a), Sin(Beta + BetaOffset - a), 0.0f)*Dist; + if(!CWorld::ProcessLineOfSight(Target, Target + ToSource, + point, ent, checkBuildings, checkVehicles, checkPeds, + checkObjects, checkDummies, true, true)) + return -a; + } + } + return 0.0f; +} + +static float DefaultAcceleration = 0.045f; +static float DefaultMaxStep = 0.15f; + +void +CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + const float GroundDist = 1.85f; + + CVector TargetCoors, Dist, IdealSource; + float Length = 0.0f; + float LateralLeft = 0.0f; + float LateralRight = 0.0f; + float Center = 0.0f; + static bool PreviouslyObscured; + static bool PickedASide; + static float FixedTargetOrientation = 0.0f; + float AngleToGoTo = 0.0f; + float BetaOffsetAvoidBuildings = 0.45f; // ~25 deg + float BetaOffsetGoingBehind = 0.45f; + bool GoingBehind = false; + bool Obscured = false; + bool BuildingCheckObscured = false; + bool HackPlayerOnStoppingTrain = false; + static int TimeIndicatedWantedToGoDown = 0; + static bool StartedCountingForGoDown = false; + float DeltaBeta; + + m_bFixingBeta = false; + bBelowMinDist = false; + bBehindPlayerDesired = false; + +#ifdef FIX_BUGS + if(!CamTargetEntity->IsPed()) + return; +#endif + assert(CamTargetEntity->IsPed()); + + // CenterDist should be > LateralDist because we don't have an angle for safety in this case + float CenterDist, LateralDist; + float AngleToGoToSpeed; + if(m_fCloseInPedHeightOffset > 0.00001f){ + LateralDist = 0.55f; + CenterDist = 1.25f; + BetaOffsetAvoidBuildings = 0.9f; // ~50 deg + BetaOffsetGoingBehind = 0.9f; + AngleToGoToSpeed = 0.88254666f; + }else{ + LateralDist = 0.8f; + CenterDist = 1.35f; + if(TheCamera.PedZoomIndicator == 1.0f || TheCamera.PedZoomIndicator == 4.0f){ + LateralDist = 1.25f; + CenterDist = 1.6f; + } + AngleToGoToSpeed = 0.43254671f; + } + + FOV = DefaultFOV; + + if(ResetStatics){ + Rotating = false; + m_bCollisionChecksOn = true; + FixedTargetOrientation = 0.0f; + PreviouslyObscured = false; + PickedASide = false; + StartedCountingForGoDown = false; + AngleToGoTo = 0.0f; + // unused LastAngleWithNoPickedASide + } + + + TargetCoors = CameraTarget; + IdealSource = Source; + TargetCoors.z += m_fSyphonModeTargetZOffSet; + + TargetCoors = DoAverageOnVector(TargetCoors); + TargetCoors.z += m_fRoadOffSet; + + Dist.x = IdealSource.x - TargetCoors.x; + Dist.y = IdealSource.y - TargetCoors.y; + Length = Dist.Magnitude2D(); + + // Cam on a string. With a fixed distance. Zoom in/out is done later. + if(Length != 0.0f) + IdealSource = TargetCoors + CVector(Dist.x, Dist.y, 0.0f)/Length * GroundDist; + else + IdealSource = TargetCoors + CVector(1.0f, 1.0f, 0.0f); + + // TODO: what's transition beta? + if(TheCamera.m_bUseTransitionBeta && ResetStatics){ + CVector VecDistance; + IdealSource.x = TargetCoors.x + GroundDist*Cos(m_fTransitionBeta); + IdealSource.y = TargetCoors.y + GroundDist*Sin(m_fTransitionBeta); + Beta = CGeneral::GetATanOfXY(IdealSource.x - TargetCoors.x, IdealSource.y - TargetCoors.y); + }else + Beta = CGeneral::GetATanOfXY(Source.x - TargetCoors.x, Source.y - TargetCoors.y); + + if(TheCamera.m_bCamDirectlyBehind){ + m_bCollisionChecksOn = true; + Beta = TargetOrientation + PI; + } + + if(FindPlayerVehicle()) + if(FindPlayerVehicle()->m_vehType == VEHICLE_TYPE_TRAIN) + HackPlayerOnStoppingTrain = true; + + if(TheCamera.m_bCamDirectlyInFront){ + m_bCollisionChecksOn = true; + Beta = TargetOrientation; + } + + while(Beta >= PI) Beta -= 2.0f * PI; + while(Beta < -PI) Beta += 2.0f * PI; + + // BUG? is this ever used? + // The values seem to be roughly m_fPedZoomValueSmooth + 1.85 + if(ResetStatics){ + if(TheCamera.PedZoomIndicator == 1.0) m_fRealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == 2.0) m_fRealGroundDist = 3.34973f; + if(TheCamera.PedZoomIndicator == 3.0) m_fRealGroundDist = 4.704914f; + if(TheCamera.PedZoomIndicator == 4.0) m_fRealGroundDist = 2.090556f; + } + // And what is this? It's only used for collision and rotation it seems + float RealGroundDist; + if(TheCamera.PedZoomIndicator == 1.0) RealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == 2.0) RealGroundDist = 3.34973f; + if(TheCamera.PedZoomIndicator == 3.0) RealGroundDist = 4.704914f; + if(TheCamera.PedZoomIndicator == 4.0) RealGroundDist = 2.090556f; + if(m_fCloseInPedHeightOffset > 0.00001f) + RealGroundDist = 1.7016f; + + + bool Shooting = false; + CPed *ped = (CPed*)CamTargetEntity; + if(ped->GetWeapon()->m_eWeaponType != WEAPONTYPE_UNARMED) + if(CPad::GetPad(0)->GetWeapon()) + Shooting = true; + if(ped->GetWeapon()->m_eWeaponType == WEAPONTYPE_DETONATOR || + ped->GetWeapon()->m_eWeaponType == WEAPONTYPE_BASEBALLBAT) + Shooting = false; + + + if(m_fCloseInPedHeightOffset > 0.00001f) + TargetCoors.z -= m_fRoadOffSet; + + // Figure out if and where we want to rotate + + if(CPad::GetPad(0)->ForceCameraBehindPlayer() || Shooting){ + + // Center cam behind player + + GoingBehind = true; + m_bCollisionChecksOn = true; + float OriginalBeta = Beta; + // Set Beta behind player + Beta = TargetOrientation + PI; + TargetCoors.z -= 0.1f; + + AngleToGoTo = GetPedBetaAngleForClearView(TargetCoors, CenterDist * RealGroundDist, 0.0f, true, false, false, true, false); + if(AngleToGoTo != 0.0f){ + if(AngleToGoTo < 0.0f) + AngleToGoTo -= AngleToGoToSpeed; + else + AngleToGoTo += AngleToGoToSpeed; + }else{ + float LateralLeft = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, BetaOffsetGoingBehind, true, false, false, true, false); + float LateralRight = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, -BetaOffsetGoingBehind, true, false, false, true, false); + if(LateralLeft == 0.0f && LateralRight != 0.0f) + AngleToGoTo += LateralRight; + else if(LateralLeft != 0.0f && LateralRight == 0.0f) + AngleToGoTo += LateralLeft; + } + + TargetCoors.z += 0.1f; + Beta = OriginalBeta; + + if(PickedASide){ + if(AngleToGoTo == 0.0f) + FixedTargetOrientation = TargetOrientation + PI; + Rotating = true; + }else{ + FixedTargetOrientation = TargetOrientation + PI + AngleToGoTo; + Rotating = true; + PickedASide = true; + } + }else{ + + // Rotate cam to avoid clipping into buildings + + TargetCoors.z -= 0.1f; + + Center = GetPedBetaAngleForClearView(TargetCoors, CenterDist * RealGroundDist, 0.0f, true, false, false, true, false); + if(m_bCollisionChecksOn || PreviouslyObscured || Center != 0.0f || m_fCloseInPedHeightOffset > 0.00001f){ + if(Center != 0.0f){ + AngleToGoTo = Center; + }else{ + LateralLeft = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, BetaOffsetAvoidBuildings, true, false, false, true, false); + LateralRight = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, -BetaOffsetAvoidBuildings, true, false, false, true, false); + if(LateralLeft == 0.0f && LateralRight != 0.0f){ + AngleToGoTo += LateralRight; + if(m_fCloseInPedHeightOffset > 0.0f) + RwCameraSetNearClipPlane(Scene.camera, 0.7f); + }else if(LateralLeft != 0.0f && LateralRight == 0.0f){ + AngleToGoTo += LateralLeft; + if(m_fCloseInPedHeightOffset > 0.0f) + RwCameraSetNearClipPlane(Scene.camera, 0.7f); + } + } + if(LateralLeft != 0.0f || LateralRight != 0.0f || Center != 0.0f) + BuildingCheckObscured = true; + } + + TargetCoors.z += 0.1f; + } + + if(m_fCloseInPedHeightOffset > 0.00001f) + TargetCoors.z += m_fRoadOffSet; + + + // Have to fix to avoid collision + + if(AngleToGoTo != 0.0f){ + Obscured = true; + Rotating = true; + if(CPad::GetPad(0)->ForceCameraBehindPlayer() || Shooting){ + if(!PickedASide) + FixedTargetOrientation = Beta + AngleToGoTo; // can this even happen? + }else + FixedTargetOrientation = Beta + AngleToGoTo; + + // This calculation is only really used to figure out how fast to rotate out of collision + + m_fAmountFractionObscured = 1.0f; + CVector PlayerPos = FindPlayerPed()->GetPosition(); + float RotationDist = (AngleToGoTo == Center ? CenterDist : LateralDist) * RealGroundDist; + // What's going on here? - AngleToGoTo? + CVector RotatedSource = PlayerPos + CVector(Cos(Beta - AngleToGoTo), Sin(Beta - AngleToGoTo), 0.0f) * RotationDist; + + CColPoint colpoint; + CEntity *entity; + if(CWorld::ProcessLineOfSight(PlayerPos, RotatedSource, colpoint, entity, true, false, false, true, false, false, false)){ + if((PlayerPos - RotatedSource).Magnitude() != 0.0f) + m_fAmountFractionObscured = (PlayerPos - colpoint.point).Magnitude() / (PlayerPos - RotatedSource).Magnitude(); + else + m_fAmountFractionObscured = 1.0f; + } + } + if(m_fAmountFractionObscured < 0.0f) m_fAmountFractionObscured = 0.0f; + if(m_fAmountFractionObscured > 1.0f) m_fAmountFractionObscured = 1.0f; + + + + // Figure out speed values for Beta rotation + + float Acceleration, MaxSpeed; + static float AccelerationMult = 0.35f; + static float MaxSpeedMult = 0.85f; + static float AccelerationMultClose = 0.7f; + static float MaxSpeedMultClose = 1.6f; + float BaseAcceleration = 0.025f; + float BaseMaxSpeed = 0.09f; + if(m_fCloseInPedHeightOffset > 0.00001f){ + if(AngleToGoTo == 0.0f){ + BaseAcceleration = 0.022f; + BaseMaxSpeed = 0.04f; + }else{ + BaseAcceleration = DefaultAcceleration; + BaseMaxSpeed = DefaultMaxStep; + } + } + if(AngleToGoTo == 0.0f){ + Acceleration = BaseAcceleration; + MaxSpeed = BaseMaxSpeed; + }else if(CPad::GetPad(0)->ForceCameraBehindPlayer() && !Shooting){ + Acceleration = 0.051f; + MaxSpeed = 0.18f; + }else if(m_fCloseInPedHeightOffset > 0.00001f){ + Acceleration = BaseAcceleration + AccelerationMultClose*sq(m_fAmountFractionObscured - 1.05f); + MaxSpeed = BaseMaxSpeed + MaxSpeedMultClose*sq(m_fAmountFractionObscured - 1.05f); + }else{ + Acceleration = DefaultAcceleration + AccelerationMult*sq(m_fAmountFractionObscured - 1.05f); + MaxSpeed = DefaultMaxStep + MaxSpeedMult*sq(m_fAmountFractionObscured - 1.05f); + } + static float AccelerationLimit = 0.3f; + static float MaxSpeedLimit = 0.65f; + if(Acceleration > AccelerationLimit) Acceleration = AccelerationLimit; + if(MaxSpeed > MaxSpeedLimit) MaxSpeed = MaxSpeedLimit; + + + int MoveState = ((CPed*)CamTargetEntity)->m_nMoveState; + if(MoveState != PEDMOVE_NONE && MoveState != PEDMOVE_STILL && + !CPad::GetPad(0)->ForceCameraBehindPlayer() && !Obscured && !Shooting){ + Rotating = false; + BetaSpeed = 0.0f; + } + + // Now do the Beta rotation + + float Distance = (IdealSource - TargetCoors).Magnitude2D(); + m_fDistanceBeforeChanges = Distance; + + if(Rotating){ + m_bFixingBeta = true; + + while(FixedTargetOrientation >= PI) FixedTargetOrientation -= 2*PI; + while(FixedTargetOrientation < -PI) FixedTargetOrientation += 2*PI; + + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + +/* + // This is inlined WellBufferMe + DeltaBeta = FixedTargetOrientation - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + + float ReqSpeed = DeltaBeta * MaxSpeed; + // Add or subtract absolute depending on sign, genius! + if(ReqSpeed - BetaSpeed > 0.0f) + BetaSpeed += SpeedStep * Abs(ReqSpeed - BetaSpeed) * CTimer::GetTimeStep(); + else + BetaSpeed -= SpeedStep * Abs(ReqSpeed - BetaSpeed) * CTimer::GetTimeStep(); + // this would be simpler: + // BetaSpeed += SpeedStep * (ReqSpeed - BetaSpeed) * CTimer::ms_fTimeStep; + + if(ReqSpeed < 0.0f && BetaSpeed < ReqSpeed) + BetaSpeed = ReqSpeed; + else if(ReqSpeed > 0.0f && BetaSpeed > ReqSpeed) + BetaSpeed = ReqSpeed; + + Beta += BetaSpeed * min(10.0f, CTimer::GetTimeStep()); +*/ + WellBufferMe(FixedTargetOrientation, &Beta, &BetaSpeed, MaxSpeed, Acceleration, true); + + if(ResetStatics){ + Beta = FixedTargetOrientation; + BetaSpeed = 0.0f; + } + + Source.x = TargetCoors.x + Distance * Cos(Beta); + Source.y = TargetCoors.y + Distance * Sin(Beta); + + // Check if we can stop rotating + DeltaBeta = FixedTargetOrientation - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < DEGTORAD(1.0f) && !bBehindPlayerDesired){ + // Stop rotation + PickedASide = false; + Rotating = false; + BetaSpeed = 0.0f; + } + } + + + if(TheCamera.m_bCamDirectlyBehind || TheCamera.m_bCamDirectlyInFront || + HackPlayerOnStoppingTrain || Rotating){ + if(TheCamera.m_bCamDirectlyBehind){ + Beta = TargetOrientation + PI; + Source.x = TargetCoors.x + Distance * Cos(Beta); + Source.y = TargetCoors.y + Distance * Sin(Beta); + } + if(TheCamera.m_bCamDirectlyInFront){ + Beta = TargetOrientation; + Source.x = TargetCoors.x + Distance * Cos(Beta); + Source.y = TargetCoors.y + Distance * Sin(Beta); + } + if(HackPlayerOnStoppingTrain){ + Beta = TargetOrientation + PI; + Source.x = TargetCoors.x + Distance * Cos(Beta); + Source.y = TargetCoors.y + Distance * Sin(Beta); + m_fDimensionOfHighestNearCar = 0.0f; + m_fCamBufferedHeight = 0.0f; + m_fCamBufferedHeightSpeed = 0.0f; + } + // Beta and Source already set in the rotation code + }else{ + Source = IdealSource; + BetaSpeed = 0.0f; + } + + // Subtract m_fRoadOffSet from both? + TargetCoors.z -= m_fRoadOffSet; + Source.z = IdealSource.z - m_fRoadOffSet; + + // Apply zoom now + // m_fPedZoomValueSmooth makes the cam go down the further out it is + // 0.25 -> 0.20 for nearest dist + // 1.50 -> -0.05 for mid dist + // 2.90 -> -0.33 for far dist + Source.z += (2.5f - TheCamera.m_fPedZoomValueSmooth)*0.2f - 0.25f; + // Zoom out camera + Front = TargetCoors - Source; + Front.Normalise(); + Source -= Front * TheCamera.m_fPedZoomValueSmooth; + // and then we move up again + // -0.375 + // 0.25 + // 0.95 + Source.z += (TheCamera.m_fPedZoomValueSmooth - 1.0f)*0.5f + m_fCloseInPedHeightOffset; + + + // Process height offset to avoid peds and cars + + float TargetZOffSet = m_fRoadOffSet + m_fDimensionOfHighestNearCar; + TargetZOffSet = max(TargetZOffSet, m_fPedBetweenCameraHeightOffset); + float TargetHeight = CameraTarget.z + TargetZOffSet - Source.z; + + if(TargetHeight > m_fCamBufferedHeight){ + // Have to go up + if(TargetZOffSet == m_fPedBetweenCameraHeightOffset && TargetZOffSet > m_fCamBufferedHeight) + WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.04f, false); + else if(TargetZOffSet == m_fRoadOffSet && TargetZOffSet > m_fCamBufferedHeight){ + // TODO: figure this out + bool foo = false; + switch(((CPhysical*)CamTargetEntity)->m_nSurfaceTouched) + case SURFACE_GRASS: + case SURFACE_DIRT: + case SURFACE_PAVEMENT: + case SURFACE_STEEL: + case SURFACE_TIRE: + case SURFACE_STONE: + foo = true; + if(foo) + WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.4f, 0.05f, false); + else + WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.025f, false); + }else + WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.025f, false); + StartedCountingForGoDown = false; + }else{ + // Have to go down + if(StartedCountingForGoDown){ + if(CTimer::GetTimeInMilliseconds() != TimeIndicatedWantedToGoDown){ + if(TargetHeight > 0.0f) + WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.01f, false); + else + WellBufferMe(0.0f, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.01f, false); + } + }else{ + StartedCountingForGoDown = true; + TimeIndicatedWantedToGoDown = CTimer::GetTimeInMilliseconds(); + } + } + + Source.z += m_fCamBufferedHeight; + + + // Clip Source if necessary + + bool ClipSource = m_fCloseInPedHeightOffset > 0.00001f && m_fCamBufferedHeight > 0.001f; + if(GoingBehind || ResetStatics || ClipSource){ + CColPoint colpoint; + CEntity *entity; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colpoint, entity, true, false, false, true, false, true, true)){ + Source = colpoint.point; + if((TargetCoors - Source).Magnitude2D() < 1.0f) + RwCameraSetNearClipPlane(Scene.camera, 0.05f); + } + } + + TargetCoors.z += min(1.0f, m_fCamBufferedHeight/2.0f); + m_cvecTargetCoorsForFudgeInter = TargetCoors; + + Front = TargetCoors - Source; + m_fRealGroundDist = Front.Magnitude2D(); + m_fMinDistAwayFromCamWhenInterPolating = m_fRealGroundDist; + Front.Normalise(); + GetVectorsReadyForRW(); + TheCamera.m_bCamDirectlyBehind = false; + TheCamera.m_bCamDirectlyInFront = false; + PreviouslyObscured = BuildingCheckObscured; + + ResetStatics = false; +} + +static float fBaseDist = 1.7f; +static float fAngleDist = 2.0f; +static float fFalloff = 3.0f; +static float fStickSens = 0.01f; +static float fTweakFOV = 1.05f; +static float fTranslateCamUp = 0.8f; +static int16 nFadeControlThreshhold = 45; +static float fDefaultAlphaOrient = -0.22f; + +void +CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + FOV = DefaultFOV; + + if(!CamTargetEntity->IsPed()) + return; + + CVector TargetCoors; + float CamDist; + CColPoint colPoint; + CEntity *entity; + + if(ResetStatics){ + Rotating = false; + m_bCollisionChecksOn = true; + CPad::GetPad(0)->ClearMouseHistory(); + ResetStatics = false; + } + + bool OnTrain = FindPlayerVehicle() && FindPlayerVehicle()->IsTrain(); + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if((MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ + UseMouse = true; + LookLeftRight = -2.5f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->LookAroundLeftRight(); + LookUpDown = CPad::GetPad(0)->LookAroundUpDown(); + } + float AlphaOffset, BetaOffset; + if(UseMouse){ + BetaOffset = LookLeftRight * TheCamera.m_fMouseAccelHorzntl * FOV/80.0f; + AlphaOffset = LookUpDown * TheCamera.m_fMouseAccelVertical * FOV/80.0f; + }else{ + BetaOffset = LookLeftRight * fStickSens * (0.5f/7.0f) * FOV/80.0f * CTimer::GetTimeStep(); + AlphaOffset = LookUpDown * fStickSens * (0.3f/7.0f) * FOV/80.0f * CTimer::GetTimeStep(); + } + + if(TheCamera.GetFading() && TheCamera.GetFadingDirection() == FADE_IN && nFadeControlThreshhold < CDraw::FadeValue || + CDraw::FadeValue > 200){ + if(Alpha < fDefaultAlphaOrient-0.05f) + AlphaOffset = 0.05f; + else if(Alpha < fDefaultAlphaOrient) + AlphaOffset = fDefaultAlphaOrient - Alpha; + else if(Alpha > fDefaultAlphaOrient+0.05f) + AlphaOffset = -0.05f; + else if(Alpha > fDefaultAlphaOrient) + AlphaOffset = fDefaultAlphaOrient - Alpha; + else + AlphaOffset = 0.0f; + } + + Alpha += AlphaOffset; + Beta += BetaOffset; + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(45.0f)) Alpha = DEGTORAD(45.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors = CameraTarget; + TargetCoors.z += fTranslateCamUp; + TargetCoors = DoAverageOnVector(TargetCoors); + + if(Alpha > fBaseDist) // comparing an angle against a distance? + CamDist = fBaseDist + Cos(min(Alpha*fFalloff, HALFPI))*fAngleDist; + else + CamDist = fBaseDist + Cos(Alpha)*fAngleDist; + + if(TheCamera.m_bUseTransitionBeta) + Beta = -CGeneral::GetATanOfXY(-Cos(m_fTransitionBeta), -Sin(m_fTransitionBeta)); + + if(TheCamera.m_bCamDirectlyBehind) + Beta = TheCamera.m_PedOrientForBehindOrInFront; + if(TheCamera.m_bCamDirectlyInFront) + Beta = TheCamera.m_PedOrientForBehindOrInFront + PI; + if(OnTrain) + Beta = TargetOrientation; + + Front.x = Cos(Alpha) * Cos(Beta); + Front.y = Cos(Alpha) * Sin(Beta); + Front.z = Sin(Alpha); + Source = TargetCoors - Front*CamDist; + m_cvecTargetCoorsForFudgeInter = TargetCoors; + + // Clip Source and fix near clip + CWorld::pIgnoreEntity = CamTargetEntity; + entity = nil; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, true, true, true, false, false, true)){ + float PedColDist = (TargetCoors - colPoint.point).Magnitude(); + float ColCamDist = CamDist - PedColDist; + if(entity->IsPed() && ColCamDist > 1.0f){ + // Ped in the way but not clipping through + if(CWorld::ProcessLineOfSight(colPoint.point, Source, colPoint, entity, true, true, true, true, false, false, true)){ + PedColDist = (TargetCoors - colPoint.point).Magnitude(); + Source = colPoint.point; + if(PedColDist < 0.9f + 0.3f) + RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); + }else{ + RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, 0.9f)); + } + }else{ + Source = colPoint.point; + if(PedColDist < 0.9f + 0.3f) + RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); + } + } + CWorld::pIgnoreEntity = nil; + + float ViewPlaneHeight = Tan(DEGTORAD(FOV) / 2.0f); + float ViewPlaneWidth = ViewPlaneHeight * CDraw::FindAspectRatio() * fTweakFOV; + float Near = RwCameraGetNearClipPlane(Scene.camera); + float radius = ViewPlaneWidth*Near; + entity = CWorld::TestSphereAgainstWorld(Source + Front*Near, radius, nil, true, true, false, true, false, false); + int i = 0; + while(entity){ + CVector CamToCol = gaTempSphereColPoints[0].point - Source; + float frontDist = DotProduct(CamToCol, Front); + float dist = (CamToCol - Front*frontDist).Magnitude() / ViewPlaneWidth; + + // Try to decrease near clip + dist = max(min(Near, dist), 0.1f); + if(dist < Near) + RwCameraSetNearClipPlane(Scene.camera, dist); + + // Move forward a bit + if(dist == 0.1f) + Source += (TargetCoors - Source)*0.3f; + +#ifndef FIX_BUGS + // this is totally wrong... + radius = Tan(FOV / 2.0f) * Near; +#endif + // Keep testing + entity = CWorld::TestSphereAgainstWorld(Source + Front*Near, radius, nil, true, true, false, true, false, false); + + i++; + if(i > 5) + entity = nil; + } + + if(CamTargetEntity->GetClump()){ + // what's going on here? + if(RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_PUMP) || + RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_THROW) || + RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_THROWU) || + RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_START_THROW)){ + CPed *player = FindPlayerPed(); + float PlayerDist = (Source - player->GetPosition()).Magnitude(); + if(PlayerDist < 2.75f) + Near = PlayerDist/2.75f * 0.9f - 0.3f; + RwCameraSetNearClipPlane(Scene.camera, max(Near, 0.1f)); + } + } + + TheCamera.m_bCamDirectlyInFront = false; + TheCamera.m_bCamDirectlyBehind = false; + + GetVectorsReadyForRW(); + + if(((CPed*)CamTargetEntity)->CanStrafeOrMouseControl() && CDraw::FadeValue < 250 && + (TheCamera.GetFadingDirection() != FADE_OUT || CDraw::FadeValue <= 100)){ + float Heading = Front.Heading(); + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Heading; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Heading; + TheCamera.pTargetEntity->SetHeading(Heading); + TheCamera.pTargetEntity->GetMatrix().UpdateRW(); + } +} + +void +CCam::Process_BehindCar(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + FOV = DefaultFOV; + + if(!CamTargetEntity->IsVehicle()) + return; + + CVector TargetCoors = CameraTarget; + TargetCoors.z -= 0.2f; + CA_MAX_DISTANCE = 9.95f; + CA_MIN_DISTANCE = 8.5f; + + CVector Dist = Source - TargetCoors; + float Length = Dist.Magnitude2D(); + m_fDistanceBeforeChanges = Length; + if(Length < 0.002f) + Length = 0.002f; + Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); + if(Length > CA_MAX_DISTANCE){ + Source.x = TargetCoors.x + Dist.x/Length * CA_MAX_DISTANCE; + Source.y = TargetCoors.y + Dist.y/Length * CA_MAX_DISTANCE; + }else if(Length < CA_MIN_DISTANCE){ + Source.x = TargetCoors.x + Dist.x/Length * CA_MIN_DISTANCE; + Source.y = TargetCoors.y + Dist.y/Length * CA_MIN_DISTANCE; + } + TargetCoors.z += 0.8f; + + WorkOutCamHeightWeeCar(TargetCoors, TargetOrientation); + RotCamIfInFrontCar(TargetCoors, TargetOrientation); + FixCamIfObscured(TargetCoors, 1.2f, TargetOrientation); + + Front = TargetCoors - Source; + m_cvecTargetCoorsForFudgeInter = TargetCoors; + ResetStatics = false; + GetVectorsReadyForRW(); +} + +void +CCam::WorkOutCamHeightWeeCar(CVector &TargetCoors, float TargetOrientation) +{ + CColPoint colpoint; + CEntity *ent; + float TargetZOffSet = 0.0f; + static bool PreviouslyFailedRoadHeightCheck = false; + static float RoadHeightFix = 0.0f; + static float RoadHeightFixSpeed = 0.0f; + + if(ResetStatics){ + RoadHeightFix = 0.0f; + RoadHeightFixSpeed = 0.0f; + Alpha = DEGTORAD(25.0f); + AlphaSpeed = 0.0f; + } + float AlphaTarget = DEGTORAD(25.0f); + if(CCullZones::CamNoRain() || CCullZones::PlayerNoRain()) + AlphaTarget = DEGTORAD(14.0f); + WellBufferMe(AlphaTarget, &Alpha, &AlphaSpeed, 0.1f, 0.05f, true); + Source.z = TargetCoors.z + CA_MAX_DISTANCE*Sin(Alpha); + + if(FindPlayerVehicle()){ + m_fRoadOffSet = 0.0f; + bool FoundRoad = false; + bool FoundRoof = false; + float RoadZ = 0.0f; + float RoofZ = 0.0f; + + if(CWorld::ProcessVerticalLine(Source, -1000.0f, colpoint, ent, true, false, false, false, false, false, nil) && + ent->IsBuilding()){ + FoundRoad = true; + RoadZ = colpoint.point.z; + } + + if(FoundRoad){ + if(Source.z - RoadZ < 0.9f){ + PreviouslyFailedRoadHeightCheck = true; + TargetZOffSet = RoadZ + 0.9f - Source.z; + }else{ + if(m_bCollisionChecksOn) + PreviouslyFailedRoadHeightCheck = false; + else + TargetZOffSet = 0.0f; + } + }else{ + if(CWorld::ProcessVerticalLine(Source, 1000.0f, colpoint, ent, true, false, false, false, false, false, nil) && + ent->IsBuilding()){ + FoundRoof = true; + RoofZ = colpoint.point.z; + } + if(FoundRoof){ + if(Source.z - RoofZ < 0.9f){ + PreviouslyFailedRoadHeightCheck = true; + TargetZOffSet = RoofZ + 0.9f - Source.z; + }else{ + if(m_bCollisionChecksOn) + PreviouslyFailedRoadHeightCheck = false; + else + TargetZOffSet = 0.0f; + } + } + } + } + + if(TargetZOffSet > RoadHeightFix) + RoadHeightFix = TargetZOffSet; + else + WellBufferMe(TargetZOffSet, &RoadHeightFix, &RoadHeightFixSpeed, 0.27f, 0.1f, false); + + if((colpoint.surfaceB == SURFACE_DEFAULT || colpoint.surfaceB >= SURFACE_METAL6) && + colpoint.surfaceB != SURFACE_STEEL && colpoint.surfaceB != SURFACE_STONE && + RoadHeightFix > 1.4f) + RoadHeightFix = 1.4f; + + Source.z += RoadHeightFix; +} + +void +CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, float TargetHeight) +{ + static float LastTargetAlphaWithCollisionOn = 0.0f; + static float LastTopAlphaSpeed = 0.0f; + static float LastAlphaSpeedStep = 0.0f; + static bool PreviousNearCheckNearClipSmall = false; + + bool CamClear = true; + float ModeAlpha = 0.0f; + + if(ResetStatics){ + LastTargetAlphaWithCollisionOn = 0.0f; + LastTopAlphaSpeed = 0.0f; + LastAlphaSpeedStep = 0.0f; + PreviousNearCheckNearClipSmall = false; + } + + float TopAlphaSpeed = 0.15f; + float AlphaSpeedStep = 0.015f; + + float zoomvalue = TheCamera.CarZoomValueSmooth; + if(zoomvalue < 0.1f) + zoomvalue = 0.1f; + if(TheCamera.CarZoomIndicator == 1.0f) + ModeAlpha = CGeneral::GetATanOfXY(23.0f, zoomvalue); // near + else if(TheCamera.CarZoomIndicator == 2.0f) + ModeAlpha = CGeneral::GetATanOfXY(10.8f, zoomvalue); // mid + else if(TheCamera.CarZoomIndicator == 3.0f) + ModeAlpha = CGeneral::GetATanOfXY(7.0f, zoomvalue); // far + + + float Length = (Source - TargetCoors).Magnitude2D(); + if(m_bCollisionChecksOn){ // there's another variable (on PC) but it's uninitialised + CVector Forward = CamTargetEntity->GetForward(); + float CarAlpha = CGeneral::GetATanOfXY(Forward.Magnitude2D(), Forward.z); + // this shouldn't be necessary.... + while(CarAlpha >= PI) CarAlpha -= 2*PI; + while(CarAlpha < -PI) CarAlpha += 2*PI; + + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + float deltaBeta = Beta - TargetOrientation; + while(deltaBeta >= PI) deltaBeta -= 2*PI; + while(deltaBeta < -PI) deltaBeta += 2*PI; + + float BehindCarNess = Cos(deltaBeta); // 1 if behind car, 0 if side, -1 if in front + CarAlpha = -CarAlpha * BehindCarNess; + if(CarAlpha < -0.01f) + CarAlpha = -0.01f; + + float DeltaAlpha = CarAlpha - Alpha; + while(DeltaAlpha >= PI) DeltaAlpha -= 2*PI; + while(DeltaAlpha < -PI) DeltaAlpha += 2*PI; + // What's this?? wouldn't it make more sense to clamp? + float AngleLimit = DEGTORAD(1.8f); + if(DeltaAlpha < -AngleLimit) + DeltaAlpha += AngleLimit; + else if(DeltaAlpha > AngleLimit) + DeltaAlpha -= AngleLimit; + else + DeltaAlpha = 0.0f; + + // Now the collision + + float TargetAlpha = 0.0f; + bool FoundRoofCenter = false; + bool FoundRoofSide1 = false; + bool FoundRoofSide2 = false; + bool FoundCamRoof = false; + bool FoundCamGround = false; + float CamRoof = 0.0f; + float CarBottom = TargetCoors.z - TargetHeight/2.0f; + + // Check car center + float CarRoof = CWorld::FindRoofZFor3DCoord(TargetCoors.x, TargetCoors.y, CarBottom, &FoundRoofCenter); + + // Check sides of the car + Forward = CamTargetEntity->GetForward(); // we actually still have that... + Forward.Normalise(); // shouldn't be necessary + float CarSideAngle = CGeneral::GetATanOfXY(Forward.x, Forward.y) + PI/2.0f; + float SideX = 2.5f * Cos(CarSideAngle); + float SideY = 2.5f * Sin(CarSideAngle); + CWorld::FindRoofZFor3DCoord(TargetCoors.x + SideX, TargetCoors.y + SideY, CarBottom, &FoundRoofSide1); + CWorld::FindRoofZFor3DCoord(TargetCoors.x - SideX, TargetCoors.y - SideY, CarBottom, &FoundRoofSide2); + + // Now find out at what height we'd like to place the camera + float CamGround = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, TargetCoors.z + Length*Sin(Alpha + ModeAlpha) + m_fCloseInCarHeightOffset, &FoundCamGround); + float CamTargetZ = 0.0f; + if(FoundCamGround){ + // This is the normal case + CamRoof = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, CamGround + TargetHeight, &FoundCamRoof); + CamTargetZ = CamGround + TargetHeight*1.5f + 0.1f; + }else{ + FoundCamRoof = false; + CamTargetZ = TargetCoors.z; + } + + if(FoundRoofCenter && !FoundCamRoof && (FoundRoofSide1 || FoundRoofSide2)){ + // Car is under something but camera isn't + // This seems weird... + TargetAlpha = CGeneral::GetATanOfXY(CA_MAX_DISTANCE, CarRoof - CamTargetZ - 1.5f); + CamClear = false; + } + if(FoundCamRoof){ + // Camera is under something + float roof = FoundRoofCenter ? min(CamRoof, CarRoof) : CamRoof; + // Same weirdness again? + TargetAlpha = CGeneral::GetATanOfXY(CA_MAX_DISTANCE, roof - CamTargetZ - 1.5f); + CamClear = false; + } + while(TargetAlpha >= PI) TargetAlpha -= 2*PI; + while(TargetAlpha < -PI) TargetAlpha += 2*PI; + if(TargetAlpha < DEGTORAD(-7.0f)) + TargetAlpha = DEGTORAD(-7.0f); + + // huh? + if(TargetAlpha > ModeAlpha) + CamClear = true; + // Camera is contrained by collision in some way + PreviousNearCheckNearClipSmall = false; + if(!CamClear){ + PreviousNearCheckNearClipSmall = true; + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + + DeltaAlpha = TargetAlpha - (Alpha + ModeAlpha); + while(DeltaAlpha >= PI) DeltaAlpha -= 2*PI; + while(DeltaAlpha < -PI) DeltaAlpha += 2*PI; + + TopAlphaSpeed = 0.3f; + AlphaSpeedStep = 0.03f; + } + + // Now do things if CamClear...but what is that anyway? + float CamZ = TargetCoors.z + Length*Sin(Alpha + DeltaAlpha + ModeAlpha) + m_fCloseInCarHeightOffset; + bool FoundGround, FoundRoof; + float CamGround2 = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, CamZ, &FoundGround); + if(FoundGround){ + if(CamClear) + if(CamZ - CamGround2 < 1.5f){ + PreviousNearCheckNearClipSmall = true; + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + + float a; + if(Length == 0.0f || CamGround2 + 1.5f - TargetCoors.z == 0.0f) + a = Alpha; + else + a = CGeneral::GetATanOfXY(Length, CamGround2 + 1.5f - TargetCoors.z); + while(a > PI) a -= 2*PI; + while(a < -PI) a += 2*PI; + DeltaAlpha = a - Alpha; + } + }else{ + if(CamClear){ + float CamRoof2 = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, CamZ, &FoundRoof); + if(FoundRoof && CamZ - CamRoof2 < 1.5f){ + PreviousNearCheckNearClipSmall = true; + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + + if(CamRoof2 > TargetCoors.z + 3.5f) + CamRoof2 = TargetCoors.z + 3.5f; + + float a; + if(Length == 0.0f || CamRoof2 + 1.5f - TargetCoors.z == 0.0f) + a = Alpha; + else + a = CGeneral::GetATanOfXY(Length, CamRoof2 + 1.5f - TargetCoors.z); + while(a > PI) a -= 2*PI; + while(a < -PI) a += 2*PI; + DeltaAlpha = a - Alpha; + } + } + } + + LastTargetAlphaWithCollisionOn = DeltaAlpha + Alpha; + LastTopAlphaSpeed = TopAlphaSpeed; + LastAlphaSpeedStep = AlphaSpeedStep; + }else{ + if(PreviousNearCheckNearClipSmall) + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + } + + WellBufferMe(LastTargetAlphaWithCollisionOn, &Alpha, &AlphaSpeed, LastTopAlphaSpeed, LastAlphaSpeedStep, true); + + Source.z = TargetCoors.z + Sin(Alpha + ModeAlpha)*Length + m_fCloseInCarHeightOffset; +} + +// Rotate cam behind the car when the car is moving forward +bool +CCam::RotCamIfInFrontCar(CVector &TargetCoors, float TargetOrientation) +{ + bool MovingForward = false; + CPhysical *phys = (CPhysical*)CamTargetEntity; + + float ForwardSpeed = DotProduct(phys->GetForward(), phys->GetSpeed(CVector(0.0f, 0.0f, 0.0f))); + if(ForwardSpeed > 0.02f) + MovingForward = true; + + float Dist = (Source - TargetCoors).Magnitude2D(); + + float DeltaBeta = TargetOrientation - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + + if(Abs(DeltaBeta) > DEGTORAD(20.0f) && MovingForward && TheCamera.m_uiTransitionState == 0) + m_bFixingBeta = true; + + CPad *pad = CPad::GetPad(0); + if(!(pad->GetLookBehindForCar() || pad->GetLookBehindForPed() || pad->GetLookLeft() || pad->GetLookRight())) + if(DirectionWasLooking != LOOKING_FORWARD) + TheCamera.m_bCamDirectlyBehind = true; + + if(!m_bFixingBeta && !TheCamera.m_bUseTransitionBeta && !TheCamera.m_bCamDirectlyBehind && !TheCamera.m_bCamDirectlyInFront) + return false; + + bool SetBeta = false; + if(TheCamera.m_bCamDirectlyBehind || TheCamera.m_bCamDirectlyInFront || TheCamera.m_bUseTransitionBeta) + if(&TheCamera.Cams[TheCamera.ActiveCam] == this) + SetBeta = true; + + if(m_bFixingBeta || SetBeta){ + WellBufferMe(TargetOrientation, &Beta, &BetaSpeed, 0.15f, 0.007f, true); + + if(TheCamera.m_bCamDirectlyBehind && &TheCamera.Cams[TheCamera.ActiveCam] == this) + Beta = TargetOrientation; + if(TheCamera.m_bCamDirectlyInFront && &TheCamera.Cams[TheCamera.ActiveCam] == this) + Beta = TargetOrientation + PI; + if(TheCamera.m_bUseTransitionBeta && &TheCamera.Cams[TheCamera.ActiveCam] == this) + Beta = m_fTransitionBeta; + + Source.x = TargetCoors.x - Cos(Beta)*Dist; + Source.y = TargetCoors.y - Sin(Beta)*Dist; + + // Check if we're done + DeltaBeta = TargetOrientation - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < DEGTORAD(2.0f)) + m_bFixingBeta = false; + } + TheCamera.m_bCamDirectlyBehind = false; + TheCamera.m_bCamDirectlyInFront = false; + return true; +} + +// Move the cam to avoid clipping through buildings +bool +CCam::FixCamIfObscured(CVector &TargetCoors, float TargetHeight, float TargetOrientation) +{ + CVector Target = TargetCoors; + bool UseEntityPos = false; + CVector EntityPos; + static CColPoint colPoint; + static bool LastObscured = false; + + if(Mode == MODE_BEHINDCAR) + Target.z += TargetHeight/2.0f; + if(Mode == MODE_CAM_ON_A_STRING){ + UseEntityPos = true; + Target.z += TargetHeight/2.0f; + EntityPos = CamTargetEntity->GetPosition(); + } + + CVector TempSource = Source; + + bool Obscured1 = false; + bool Obscured2 = false; + bool Fix1 = false; + float Dist1 = 0.0f; + float Dist2 = 0.0f; + CEntity *ent; + if(m_bCollisionChecksOn || LastObscured){ + Obscured1 = CWorld::ProcessLineOfSight(Target, TempSource, colPoint, ent, true, false, false, true, false, true, true); + if(Obscured1){ + Dist1 = (Target - colPoint.point).Magnitude2D(); + Fix1 = true; + if(UseEntityPos) + Obscured1 = CWorld::ProcessLineOfSight(EntityPos, TempSource, colPoint, ent, true, false, false, true, false, true, true); + }else if(m_bFixingBeta){ + float d = (TempSource - Target).Magnitude(); + TempSource.x = Target.x - d*Cos(TargetOrientation); + TempSource.y = Target.y - d*Sin(TargetOrientation); + + // same check again + Obscured2 = CWorld::ProcessLineOfSight(Target, TempSource, colPoint, ent, true, false, false, true, false, true, true); + if(Obscured2){ + Dist2 = (Target - colPoint.point).Magnitude2D(); + if(UseEntityPos) + Obscured2 = CWorld::ProcessLineOfSight(EntityPos, TempSource, colPoint, ent, true, false, false, true, false, true, true); + } + } + LastObscured = Obscured1 || Obscured2; + } + + // nothing to do + if(!LastObscured) + return false; + + if(Fix1){ + Source.x = Target.x - Cos(Beta)*Dist1; + Source.y = Target.y - Sin(Beta)*Dist1; + if(Mode == MODE_BEHINDCAR) + Source = colPoint.point; + }else{ + WellBufferMe(Dist2, &m_fDistanceBeforeChanges, &DistanceSpeed, 0.2f, 0.025f, false); + Source.x = Target.x - Cos(Beta)*m_fDistanceBeforeChanges; + Source.y = Target.y - Sin(Beta)*m_fDistanceBeforeChanges; + } + + if(ResetStatics){ + m_fDistanceBeforeChanges = (Source - Target).Magnitude2D(); + DistanceSpeed = 0.0f; + Source.x = colPoint.point.x; + Source.y = colPoint.point.y; + } + return true; +} + +void +CCam::Process_Cam_On_A_String(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + if(!CamTargetEntity->IsVehicle()) + return; + + FOV = DefaultFOV; + + if(ResetStatics){ + AlphaSpeed = 0.0f; + if(TheCamera.m_bIdleOn) + TheCamera.m_uiTimeWeEnteredIdle = CTimer::GetTimeInMilliseconds(); + } + + CBaseModelInfo *mi = CModelInfo::GetModelInfo(CamTargetEntity->GetModelIndex()); + CVector Dimensions = mi->GetColModel()->boundingBox.max - mi->GetColModel()->boundingBox.min; + float BaseDist = Dimensions.Magnitude2D(); + + CVector TargetCoors = CameraTarget; + TargetCoors.z += Dimensions.z - 0.1f; // final + Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); + while(Alpha >= PI) Alpha -= 2*PI; + while(Alpha < -PI) Alpha += 2*PI; + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + m_fDistanceBeforeChanges = (Source - TargetCoors).Magnitude2D(); + + Cam_On_A_String_Unobscured(TargetCoors, BaseDist); + WorkOutCamHeight(TargetCoors, TargetOrientation, Dimensions.z); + RotCamIfInFrontCar(TargetCoors, TargetOrientation); + FixCamIfObscured(TargetCoors, Dimensions.z, TargetOrientation); + FixCamWhenObscuredByVehicle(TargetCoors); + + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + Front.Normalise(); + GetVectorsReadyForRW(); + ResetStatics = false; +} + +// Basic Cam on a string algorithm +void +CCam::Cam_On_A_String_Unobscured(const CVector &TargetCoors, float BaseDist) +{ + CA_MAX_DISTANCE = BaseDist + 0.1f + TheCamera.CarZoomValueSmooth; + CA_MIN_DISTANCE = min(BaseDist*0.6f, 3.5f); + + CVector Dist = Source - TargetCoors; + + if(ResetStatics) + Source = TargetCoors + Dist*(CA_MAX_DISTANCE + 1.0f); + + float Length = Dist.Magnitude2D(); + if(Length < 0.001f){ + // This probably shouldn't happen. reset view + CVector Forward = CamTargetEntity->GetForward(); + Forward.z = 0.0f; + Forward.Normalise(); + Source = TargetCoors - Forward*CA_MAX_DISTANCE; + Dist = Source - TargetCoors; + Length = Dist.Magnitude2D(); + } + + if(Length > CA_MAX_DISTANCE){ + Source.x = TargetCoors.x + Dist.x/Length * CA_MAX_DISTANCE; + Source.y = TargetCoors.y + Dist.y/Length * CA_MAX_DISTANCE; + }else if(Length < CA_MIN_DISTANCE){ + Source.x = TargetCoors.x + Dist.x/Length * CA_MIN_DISTANCE; + Source.y = TargetCoors.y + Dist.y/Length * CA_MIN_DISTANCE; + } +} + +void +CCam::FixCamWhenObscuredByVehicle(const CVector &TargetCoors) +{ + // BUG? is this never reset + static float HeightFixerCarsObscuring = 0.0f; + static float HeightFixerCarsObscuringSpeed = 0.0f; + CColPoint colPoint; + CEntity *entity; + + float HeightTarget = 0.0f; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, false, true, false, false, false, false, false)){ + CBaseModelInfo *mi = CModelInfo::GetModelInfo(entity->GetModelIndex()); + HeightTarget = mi->GetColModel()->boundingBox.max.z + 1.0f + TargetCoors.z - Source.z; + if(HeightTarget < 0.0f) + HeightTarget = 0.0f; + } + WellBufferMe(HeightTarget, &HeightFixerCarsObscuring, &HeightFixerCarsObscuringSpeed, 0.2f, 0.025f, false); + Source.z += HeightFixerCarsObscuring; +} + +void +CCam::Process_TopDown(const CVector &CameraTarget, float TargetOrientation, float SpeedVar, float TargetSpeedVar) +{ + FOV = DefaultFOV; + + if(!CamTargetEntity->IsVehicle()) + return; + + float Dist; + float HeightTarget = 0.0f; + static float AdjustHeightTargetMoveBuffer = 0.0f; + static float AdjustHeightTargetMoveSpeed = 0.0f; + static float NearClipDistance = 1.5f; + const float FarClipDistance = 200.0f; + CVector TargetFront, Target; + CVector TestSource, TestTarget; + CColPoint colPoint; + CEntity *entity; + + TargetFront = CameraTarget; + TargetFront.x += 18.0f*CamTargetEntity->GetForward().x*SpeedVar; + TargetFront.y += 18.0f*CamTargetEntity->GetForward().y*SpeedVar; + + if(ResetStatics){ + AdjustHeightTargetMoveBuffer = 0.0f; + AdjustHeightTargetMoveSpeed = 0.0f; + } + + float f = Pow(0.8f, 4.0f); + Target = f*CameraTarget + (1.0f-f)*TargetFront; + if(Mode == MODE_GTACLASSIC) + SpeedVar = TargetSpeedVar; + Source = Target + CVector(0.0f, 0.0f, (40.0f*SpeedVar + 30.0f)*0.8f); + // What is this? looks horrible + if(Mode == MODE_GTACLASSIC) + Source.x += (uint8)(100.0f*CameraTarget.x)/500.0f; + + TestSource = Source; + TestTarget = TestSource; + TestTarget.z = Target.z; + if(CWorld::ProcessLineOfSight(TestTarget, TestSource, colPoint, entity, true, false, false, false, false, false, false)){ + if(Source.z < colPoint.point.z+3.0f) + HeightTarget = colPoint.point.z+3.0f - Source.z; + }else{ + TestSource = Source; + TestTarget = TestSource; + TestTarget.z += 10.0f; + if(CWorld::ProcessLineOfSight(TestTarget, TestSource, colPoint, entity, true, false, false, false, false, false, false)) + if(Source.z < colPoint.point.z+3.0f) + HeightTarget = colPoint.point.z+3.0f - Source.z; + } + WellBufferMe(HeightTarget, &AdjustHeightTargetMoveBuffer, &AdjustHeightTargetMoveSpeed, 0.2f, 0.02f, false); + Source.z += AdjustHeightTargetMoveBuffer; + + if(RwCameraGetFarClipPlane(Scene.camera) > FarClipDistance) + RwCameraSetFarClipPlane(Scene.camera, FarClipDistance); + RwCameraSetNearClipPlane(Scene.camera, NearClipDistance); + + Front = CVector(-0.01f, -0.01f, -1.0f); // look down + Front.Normalise(); + Dist = (Source - CameraTarget).Magnitude(); + m_cvecTargetCoorsForFudgeInter = Dist*Front + Source; + Up = CVector(0.0f, 1.0f, 0.0f); + + ResetStatics = false; +} + +void +CCam::AvoidWallsTopDownPed(const CVector &TargetCoors, const CVector &Offset, float *Adjuster, float *AdjusterSpeed, float yDistLimit) +{ + float Target = 0.0f; + float MaxSpeed = 0.13f; + float Acceleration = 0.015f; + float SpeedMult; + float dy; + CVector TestPoint2; + CVector TestPoint1; + CColPoint colPoint; + CEntity *entity; + + TestPoint2 = TargetCoors + Offset; + TestPoint1 = TargetCoors; + TestPoint1.z = TestPoint2.z; + if(CWorld::ProcessLineOfSight(TestPoint1, TestPoint2, colPoint, entity, true, false, false, false, false, false, false)){ + // What is this even? + dy = TestPoint1.y - colPoint.point.y; + if(dy > yDistLimit) + dy = yDistLimit; + SpeedMult = yDistLimit - Abs(dy/yDistLimit); + + Target = 2.5f; + MaxSpeed += SpeedMult*0.3f; + Acceleration += SpeedMult*0.03f; + } + WellBufferMe(Target, Adjuster, AdjusterSpeed, MaxSpeed, Acceleration, false); +} + +void +CCam::Process_TopDownPed(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + if(!CamTargetEntity->IsPed()) + return; + + float Dist; + float HeightTarget; + static int NumPedPosCountsSoFar = 0; + static float PedAverageSpeed = 0.0f; + static float AdjustHeightTargetMoveBuffer = 0.0f; + static float AdjustHeightTargetMoveSpeed = 0.0f; + static float PedSpeedSoFar = 0.0f; + static float FarClipDistance = 200.0f; + static float NearClipDistance = 1.5f; + static float TargetAdjusterForSouth = 0.0f; + static float TargetAdjusterSpeedForSouth = 0.0f; + static float TargetAdjusterForNorth = 0.0f; + static float TargetAdjusterSpeedForNorth = 0.0f; + static float TargetAdjusterForEast = 0.0f; + static float TargetAdjusterSpeedForEast = 0.0f; + static float TargetAdjusterForWest = 0.0f; + static float TargetAdjusterSpeedForWest = 0.0f; + static CVector PreviousPlayerMoveSpeedVec; + CVector TargetCoors, PlayerMoveSpeed; + CVector TestSource, TestTarget; + CColPoint colPoint; + CEntity *entity; + + FOV = DefaultFOV; + TargetCoors = CameraTarget; + PlayerMoveSpeed = ((CPed*)CamTargetEntity)->GetMoveSpeed(); + + if(ResetStatics){ + PreviousPlayerMoveSpeedVec = PlayerMoveSpeed; + AdjustHeightTargetMoveBuffer = 0.0f; + AdjustHeightTargetMoveSpeed = 0.0f; + NumPedPosCountsSoFar = 0; + PedSpeedSoFar = 0.0f; + PedAverageSpeed = 0.0f; + TargetAdjusterForWest = 0.0f; + TargetAdjusterSpeedForWest = 0.0f; + TargetAdjusterForEast = 0.0f; + TargetAdjusterSpeedForEast = 0.0f; + TargetAdjusterForNorth = 0.0f; + TargetAdjusterSpeedForNorth = 0.0f; + TargetAdjusterForSouth = 0.0f; + TargetAdjusterSpeedForSouth = 0.0f; + } + + if(RwCameraGetFarClipPlane(Scene.camera) > FarClipDistance) + RwCameraSetFarClipPlane(Scene.camera, FarClipDistance); + RwCameraSetNearClipPlane(Scene.camera, NearClipDistance); + + // Average ped speed + NumPedPosCountsSoFar++; + PedSpeedSoFar += PlayerMoveSpeed.Magnitude(); + if(NumPedPosCountsSoFar == 5){ + PedAverageSpeed = 0.4f*PedAverageSpeed + 0.6*(PedSpeedSoFar/5.0f); + NumPedPosCountsSoFar = 0; + PedSpeedSoFar = 0.0f; + } + PreviousPlayerMoveSpeedVec = PlayerMoveSpeed; + + // Zoom out depending on speed + if(PedAverageSpeed > 0.01f && PedAverageSpeed <= 0.04f) + HeightTarget = 2.5f; + else if(PedAverageSpeed > 0.04f && PedAverageSpeed <= 0.145f) + HeightTarget = 4.5f; + else if(PedAverageSpeed > 0.145f) + HeightTarget = 7.0f; + else + HeightTarget = 0.0f; + + // Zoom out if locked on target is far away + if(FindPlayerPed()->m_pPointGunAt){ + Dist = (FindPlayerPed()->m_pPointGunAt->GetPosition() - CameraTarget).Magnitude2D(); + if(Dist > 6.0f) + HeightTarget = max(HeightTarget, Dist/22.0f*37.0f); + } + + Source = TargetCoors + CVector(0.0f, -1.0f, 9.0f); + + // Collision checks + entity = nil; + TestSource = TargetCoors + CVector(0.0f, -1.0f, 9.0f); + TestTarget = TestSource; + TestTarget.z = TargetCoors.z; + if(CWorld::ProcessLineOfSight(TestTarget, TestSource, colPoint, entity, true, false, false, false, false, false, false)){ + if(TargetCoors.z+9.0f+HeightTarget < colPoint.point.z+3.0f) + HeightTarget = colPoint.point.z+3.0f - (TargetCoors.z+9.0f); + }else{ + TestSource = TargetCoors + CVector(0.0f, -1.0f, 9.0f); + TestTarget = TestSource; + TestSource.z += HeightTarget; + TestTarget.z = TestSource.z + 10.0f; + if(CWorld::ProcessLineOfSight(TestTarget, TestSource, colPoint, entity, true, false, false, false, false, false, false)){ + if(TargetCoors.z+9.0f+HeightTarget < colPoint.point.z+3.0f) + HeightTarget = colPoint.point.z+3.0f - (TargetCoors.z+9.0f); + } + } + + WellBufferMe(HeightTarget, &AdjustHeightTargetMoveBuffer, &AdjustHeightTargetMoveSpeed, 0.3f, 0.03f, false); + Source.z += AdjustHeightTargetMoveBuffer; + + // Wall checks + AvoidWallsTopDownPed(TargetCoors, CVector(0.0f, -3.0f, 3.0f), &TargetAdjusterForSouth, &TargetAdjusterSpeedForSouth, 1.0f); + Source.y += TargetAdjusterForSouth; + AvoidWallsTopDownPed(TargetCoors, CVector(0.0f, 3.0f, 3.0f), &TargetAdjusterForNorth, &TargetAdjusterSpeedForNorth, 1.0f); + Source.y -= TargetAdjusterForNorth; + // BUG: east and west flipped + AvoidWallsTopDownPed(TargetCoors, CVector(3.0f, 0.0f, 3.0f), &TargetAdjusterForWest, &TargetAdjusterSpeedForWest, 1.0f); + Source.x -= TargetAdjusterForWest; + AvoidWallsTopDownPed(TargetCoors, CVector(-3.0f, 0.0f, 3.0f), &TargetAdjusterForEast, &TargetAdjusterSpeedForEast, 1.0f); + Source.x += TargetAdjusterForEast; + + TargetCoors.y = Source.y + 1.0f; + TargetCoors.y += TargetAdjusterForSouth; + TargetCoors.x += TargetAdjusterForEast; + TargetCoors.x -= TargetAdjusterForWest; + + Front = TargetCoors - Source; + Front.Normalise(); +#ifdef FIX_BUGS + if(Front.x == 0.0f && Front.y == 0.0f) + Front.y = 0.0001f; +#else + // someone used = instead of == in the above check by accident + Front.x = 0.0f; +#endif + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Up = CrossProduct(Front, CVector(-1.0f, 0.0f, 0.0f)); + Up.Normalise(); + + ResetStatics = false; +} + +// Identical to M16 +void +CCam::Process_Rocket(const CVector &CameraTarget, float, float, float) +{ + if(!CamTargetEntity->IsPed()) + return; + + static bool FailedTestTwelveFramesAgo = false; + RwV3d HeadPos; + CVector TargetCoors; + + FOV = DefaultFOV; + TargetCoors = CameraTarget; + + if(ResetStatics){ + Beta = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + Alpha = 0.0f; + m_fInitialPlayerOrientation = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + FailedTestTwelveFramesAgo = false; + // static DPadVertical unused + // static DPadHorizontal unused + m_bCollisionChecksOn = true; + ResetStatics = false; + } + + ((CPed*)CamTargetEntity)->m_pedIK.GetComponentPosition(&HeadPos, PED_HEAD); + Source = HeadPos; + Source.z += 0.1f; + Source.x -= 0.19f*Cos(m_fInitialPlayerOrientation); + Source.y -= 0.19f*Sin(m_fInitialPlayerOrientation); + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if(MouseX != 0.0f || MouseY != 0.0f){ + UseMouse = true; + LookLeftRight = -3.0f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->SniperModeLookLeftRight(); + LookUpDown = CPad::GetPad(0)->SniperModeLookUpDown(); + } + if(UseMouse){ + Beta += TheCamera.m_fMouseAccelHorzntl * LookLeftRight * FOV/80.0f; + Alpha += TheCamera.m_fMouseAccelVertical * LookUpDown * FOV/80.0f; + }else{ + float xdir = LookLeftRight < 0.0f ? -1.0f : 1.0f; + float ydir = LookUpDown < 0.0f ? -1.0f : 1.0f; + Beta += SQR(LookLeftRight/100.0f)*xdir/17.5 * FOV/80.0f * CTimer::GetTimeStep(); + Alpha += SQR(LookUpDown/150.0f)*ydir/14.0f * FOV/80.0f * CTimer::GetTimeStep(); + } + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(60.0f)) Alpha = DEGTORAD(60.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors.x = 3.0f * Cos(Alpha) * Cos(Beta) + Source.x; + TargetCoors.y = 3.0f * Cos(Alpha) * Sin(Beta) + Source.y; + TargetCoors.z = 3.0f * Sin(Alpha) + Source.z; + Front = TargetCoors - Source; + Front.Normalise(); + Source += Front*0.4f; + + if(m_bCollisionChecksOn){ + if(!CWorld::GetIsLineOfSightClear(TargetCoors, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + CVector TestPoint; + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta + DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta + DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta - DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta - DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else + FailedTestTwelveFramesAgo = false; + } + } + } + + if(FailedTestTwelveFramesAgo) + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source -= Front*0.4f; + + GetVectorsReadyForRW(); + float Rotation = CGeneral::GetATanOfXY(Front.x, Front.y) - HALFPI; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Rotation; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Rotation; +} + +// Identical to Rocket +void +CCam::Process_M16_1stPerson(const CVector &CameraTarget, float, float, float) +{ + if(!CamTargetEntity->IsPed()) + return; + + static bool FailedTestTwelveFramesAgo = false; + RwV3d HeadPos; + CVector TargetCoors; + + FOV = DefaultFOV; + TargetCoors = CameraTarget; + + if(ResetStatics){ + Beta = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + Alpha = 0.0f; + m_fInitialPlayerOrientation = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + FailedTestTwelveFramesAgo = false; + // static DPadVertical unused + // static DPadHorizontal unused + m_bCollisionChecksOn = true; + ResetStatics = false; + } + + ((CPed*)CamTargetEntity)->m_pedIK.GetComponentPosition(&HeadPos, PED_HEAD); + Source = HeadPos; + Source.z += 0.1f; + Source.x -= 0.19f*Cos(m_fInitialPlayerOrientation); + Source.y -= 0.19f*Sin(m_fInitialPlayerOrientation); + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if(MouseX != 0.0f || MouseY != 0.0f){ + UseMouse = true; + LookLeftRight = -3.0f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->SniperModeLookLeftRight(); + LookUpDown = CPad::GetPad(0)->SniperModeLookUpDown(); + } + if(UseMouse){ + Beta += TheCamera.m_fMouseAccelHorzntl * LookLeftRight * FOV/80.0f; + Alpha += TheCamera.m_fMouseAccelVertical * LookUpDown * FOV/80.0f; + }else{ + float xdir = LookLeftRight < 0.0f ? -1.0f : 1.0f; + float ydir = LookUpDown < 0.0f ? -1.0f : 1.0f; + Beta += SQR(LookLeftRight/100.0f)*xdir/17.5 * FOV/80.0f * CTimer::GetTimeStep(); + Alpha += SQR(LookUpDown/150.0f)*ydir/14.0f * FOV/80.0f * CTimer::GetTimeStep(); + } + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(60.0f)) Alpha = DEGTORAD(60.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors.x = 3.0f * Cos(Alpha) * Cos(Beta) + Source.x; + TargetCoors.y = 3.0f * Cos(Alpha) * Sin(Beta) + Source.y; + TargetCoors.z = 3.0f * Sin(Alpha) + Source.z; + Front = TargetCoors - Source; + Front.Normalise(); + Source += Front*0.4f; + + if(m_bCollisionChecksOn){ + if(!CWorld::GetIsLineOfSightClear(TargetCoors, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + CVector TestPoint; + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta + DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta + DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta - DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta - DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else + FailedTestTwelveFramesAgo = false; + } + } + } + + if(FailedTestTwelveFramesAgo) + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source -= Front*0.4f; + + GetVectorsReadyForRW(); + float Rotation = CGeneral::GetATanOfXY(Front.x, Front.y) - HALFPI; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Rotation; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Rotation; +} + +void +CCam::Process_1stPerson(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + static float DontLookThroughWorldFixer = 0.0f; + CVector TargetCoors; + + FOV = DefaultFOV; + TargetCoors = CameraTarget; + if(CamTargetEntity->m_rwObject == nil) + return; + + if(ResetStatics){ + Beta = TargetOrientation; + Alpha = 0.0f; + m_fInitialPlayerOrientation = TargetOrientation; + if(CamTargetEntity->IsPed()){ + Beta = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + Alpha = 0.0f; + m_fInitialPlayerOrientation = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + } + DontLookThroughWorldFixer = 0.0f; + } + + if(CamTargetEntity->IsPed()){ + static bool FailedTestTwelveFramesAgo = false; + RwV3d HeadPos; + + TargetCoors = CameraTarget; + + if(ResetStatics){ + Beta = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + Alpha = 0.0f; + m_fInitialPlayerOrientation = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + FailedTestTwelveFramesAgo = false; + // static DPadVertical unused + // static DPadHorizontal unused + m_bCollisionChecksOn = true; + ResetStatics = false; + } + + ((CPed*)CamTargetEntity)->m_pedIK.GetComponentPosition(&HeadPos, PED_HEAD); + Source = HeadPos; + Source.z += 0.1f; + Source.x -= 0.19f*Cos(m_fInitialPlayerOrientation); + Source.y -= 0.19f*Sin(m_fInitialPlayerOrientation); + + float LookLeftRight, LookUpDown; + LookLeftRight = -CPad::GetPad(0)->LookAroundLeftRight(); + LookUpDown = CPad::GetPad(0)->LookAroundUpDown(); + float xdir = LookLeftRight < 0.0f ? -1.0f : 1.0f; + float ydir = LookUpDown < 0.0f ? -1.0f : 1.0f; + Beta += SQR(LookLeftRight/100.0f)*xdir/17.5 * FOV/80.0f * CTimer::GetTimeStep(); + Alpha += SQR(LookUpDown/150.0f)*ydir/14.0f * FOV/80.0f * CTimer::GetTimeStep(); + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(60.0f)) Alpha = DEGTORAD(60.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors.x = 3.0f * Cos(Alpha) * Cos(Beta) + Source.x; + TargetCoors.y = 3.0f * Cos(Alpha) * Sin(Beta) + Source.y; + TargetCoors.z = 3.0f * Sin(Alpha) + Source.z; + Front = TargetCoors - Source; + Front.Normalise(); + Source += Front*0.4f; + + if(m_bCollisionChecksOn){ + if(!CWorld::GetIsLineOfSightClear(TargetCoors, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + CVector TestPoint; + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta + DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta + DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta - DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta - DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else + FailedTestTwelveFramesAgo = false; + } + } + } + + if(FailedTestTwelveFramesAgo) + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source -= Front*0.4f; + + GetVectorsReadyForRW(); + float Rotation = CGeneral::GetATanOfXY(Front.x, Front.y) - HALFPI; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Rotation; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Rotation; + }else{ + assert(CamTargetEntity->IsVehicle()); + CVehicleModelInfo *mi = (CVehicleModelInfo*)CModelInfo::GetModelInfo(CamTargetEntity->GetModelIndex()); + CVector CamPos = mi->m_vehicleType == VEHICLE_TYPE_BOAT ? mi->m_positions[BOAT_POS_FRONTSEAT] : mi->m_positions[CAR_POS_FRONTSEAT]; + CamPos.x = 0.0f; + CamPos.y += -0.08f; + CamPos.z += 0.62f; + FOV = 60.0f; + Source = Multiply3x3(CamTargetEntity->GetMatrix(), CamPos); + Source += CamTargetEntity->GetPosition(); + if(((CVehicle*)CamTargetEntity)->IsBoat()) + Source.z += 0.5f; + + if(((CVehicle*)CamTargetEntity)->IsUpsideDown()){ + if(DontLookThroughWorldFixer < 0.5f) + DontLookThroughWorldFixer += 0.03f; + else + DontLookThroughWorldFixer = 0.5f; + }else{ + if(DontLookThroughWorldFixer < 0.0f) +#ifdef FIX_BUGS + DontLookThroughWorldFixer += 0.03f; +#else + DontLookThroughWorldFixer -= 0.03f; +#endif + else + DontLookThroughWorldFixer = 0.0f; + } + Source.z += DontLookThroughWorldFixer; + Front = CamTargetEntity->GetForward(); + Front.Normalise(); + Up = CamTargetEntity->GetUp(); + Up.Normalise(); + CVector Right = CrossProduct(Front, Up); + Right.Normalise(); + Up = CrossProduct(Right, Front); + Up.Normalise(); + } + + ResetStatics = false; +} + +static CVector vecHeadCamOffset(0.06f, 0.05f, 0.0f); + +void +CCam::Process_1rstPersonPedOnPC(const CVector&, float TargetOrientation, float, float) +{ + // static int DontLookThroughWorldFixer = 0; // unused + static CVector InitialHeadPos; + + if(Mode != MODE_SNIPER_RUNABOUT) + FOV = DefaultFOV; + TheCamera.m_1rstPersonRunCloseToAWall = false; + if(CamTargetEntity->m_rwObject == nil) + return; + + if(CamTargetEntity->IsPed()){ + // static bool FailedTestTwelveFramesAgo = false; // unused + RwV3d HeadPos = vecHeadCamOffset; + CVector TargetCoors; + + // needs fix for SKINNING + RwFrame *frm = ((CPed*)CamTargetEntity)->GetNodeFrame(PED_HEAD); + while(frm){ + RwV3dTransformPoints(&HeadPos, &HeadPos, 1, RwFrameGetMatrix(frm)); + frm = RwFrameGetParent(frm); + if(frm == RpClumpGetFrame(CamTargetEntity->GetClump())) + frm = nil; + } + + if(ResetStatics){ + Beta = TargetOrientation; + Alpha = 0.0f; + m_fInitialPlayerOrientation = TargetOrientation; + if(CamTargetEntity->IsPed()){ // useless check + Beta = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + Alpha = 0.0f; + m_fInitialPlayerOrientation = ((CPed*)CamTargetEntity)->m_fRotationCur + HALFPI; + // FailedTestTwelveFramesAgo = false; + m_bCollisionChecksOn = true; + } + // DontLookThroughWorldFixer = false; + m_vecBufferedPlayerBodyOffset = HeadPos; + InitialHeadPos = HeadPos; + } + + m_vecBufferedPlayerBodyOffset.y = HeadPos.y; + + if(TheCamera.m_bHeadBob){ + m_vecBufferedPlayerBodyOffset.x = + TheCamera.m_fGaitSwayBuffer * m_vecBufferedPlayerBodyOffset.x + + (1.0f-TheCamera.m_fGaitSwayBuffer) * HeadPos.x; + m_vecBufferedPlayerBodyOffset.z = + TheCamera.m_fGaitSwayBuffer * m_vecBufferedPlayerBodyOffset.z + + (1.0f-TheCamera.m_fGaitSwayBuffer) * HeadPos.z; + HeadPos = RwV3d(CamTargetEntity->GetMatrix() * m_vecBufferedPlayerBodyOffset); + }else{ + float HeadDelta = (HeadPos - InitialHeadPos).Magnitude2D(); + CVector Fwd = CamTargetEntity->GetForward(); + Fwd.z = 0.0f; + Fwd.Normalise(); + HeadPos = RwV3d(HeadDelta*1.23f*Fwd + CamTargetEntity->GetPosition()); + HeadPos.z += 0.59f; + } + Source = HeadPos; + + // unused: + // ((CPed*)CamTargetEntity)->m_pedIK.GetComponentPosition(&MidPos, PED_MID); + // Source - MidPos; + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if(MouseX != 0.0f || MouseY != 0.0f){ + UseMouse = true; + LookLeftRight = -3.0f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->LookAroundLeftRight(); + LookUpDown = CPad::GetPad(0)->LookAroundUpDown(); + } + if(UseMouse){ + Beta += TheCamera.m_fMouseAccelHorzntl * LookLeftRight * FOV/80.0f; + Alpha += TheCamera.m_fMouseAccelVertical * LookUpDown * FOV/80.0f; + }else{ + float xdir = LookLeftRight < 0.0f ? -1.0f : 1.0f; + float ydir = LookUpDown < 0.0f ? -1.0f : 1.0f; + Beta += SQR(LookLeftRight/100.0f)*xdir/17.5 * FOV/80.0f * CTimer::GetTimeStep(); + Alpha += SQR(LookUpDown/150.0f)*ydir/14.0f * FOV/80.0f * CTimer::GetTimeStep(); + } + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(60.0f)) Alpha = DEGTORAD(60.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors.x = 3.0f * Cos(Alpha) * Cos(Beta) + Source.x; + TargetCoors.y = 3.0f * Cos(Alpha) * Sin(Beta) + Source.y; + TargetCoors.z = 3.0f * Sin(Alpha) + Source.z; + Front = TargetCoors - Source; + Front.Normalise(); + Source += Front*0.4f; + + TheCamera.m_AlphaForPlayerAnim1rstPerson = Alpha; + + GetVectorsReadyForRW(); + + float Heading = Front.Heading(); + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Heading; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Heading; + TheCamera.pTargetEntity->SetHeading(Heading); + TheCamera.pTargetEntity->GetMatrix().UpdateRW(); + + if(Mode == MODE_SNIPER_RUNABOUT){ + // no mouse wheel FOV buffering here like in normal sniper mode + if(CPad::GetPad(0)->SniperZoomIn() || CPad::GetPad(0)->SniperZoomOut()){ + if(CPad::GetPad(0)->SniperZoomOut()) + FOV *= (255.0f*CTimer::GetTimeStep() + 10000.0f) / 10000.0f; + else + FOV /= (255.0f*CTimer::GetTimeStep() + 10000.0f) / 10000.0f; + } + + TheCamera.SetMotionBlur(180, 255, 180, 120, MBLUR_SNIPER); + + if(FOV > DefaultFOV) + FOV = DefaultFOV; + if(FOV < 15.0f) + FOV = 15.0f; + } + } + + ResetStatics = false; + RwCameraSetNearClipPlane(Scene.camera, 0.05f); +} + +void +CCam::Process_Sniper(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + if(CamTargetEntity->m_rwObject == nil) + return; + +#ifdef FIX_BUGS + if(!CamTargetEntity->IsPed()) + return; +#endif + + static bool FailedTestTwelveFramesAgo = false; + RwV3d HeadPos; + CVector TargetCoors; + TargetCoors = CameraTarget; + + static float TargetFOV = 0.0f; + + if(ResetStatics){ + Beta = TargetOrientation; + Alpha = 0.0f; + m_fInitialPlayerOrientation = TargetOrientation; + FailedTestTwelveFramesAgo = false; + // static DPadVertical unused + // static DPadHorizontal unused + m_bCollisionChecksOn = true; + FOVSpeed = 0.0f; + TargetFOV = FOV; + ResetStatics = false; + } + + ((CPed*)CamTargetEntity)->m_pedIK.GetComponentPosition(&HeadPos, PED_HEAD); + Source = HeadPos; + Source.z += 0.1f; + Source.x -= 0.19f*Cos(m_fInitialPlayerOrientation); + Source.y -= 0.19f*Sin(m_fInitialPlayerOrientation); + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if(MouseX != 0.0f || MouseY != 0.0f){ + UseMouse = true; + LookLeftRight = -3.0f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->SniperModeLookLeftRight(); + LookUpDown = CPad::GetPad(0)->SniperModeLookUpDown(); + } + if(UseMouse){ + Beta += TheCamera.m_fMouseAccelHorzntl * LookLeftRight * FOV/80.0f; + Alpha += TheCamera.m_fMouseAccelVertical * LookUpDown * FOV/80.0f; + }else{ + float xdir = LookLeftRight < 0.0f ? -1.0f : 1.0f; + float ydir = LookUpDown < 0.0f ? -1.0f : 1.0f; + Beta += SQR(LookLeftRight/100.0f)*xdir/17.5 * FOV/80.0f * CTimer::GetTimeStep(); + Alpha += SQR(LookUpDown/150.0f)*ydir/14.0f * FOV/80.0f * CTimer::GetTimeStep(); + } + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + if(Alpha > DEGTORAD(60.0f)) Alpha = DEGTORAD(60.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + TargetCoors.x = 3.0f * Cos(Alpha) * Cos(Beta) + Source.x; + TargetCoors.y = 3.0f * Cos(Alpha) * Sin(Beta) + Source.y; + TargetCoors.z = 3.0f * Sin(Alpha) + Source.z; + + UseMouse = false; + int ZoomInButton = ControlsManager.GetMouseButtonAssociatedWithAction(PED_SNIPER_ZOOM_IN); + int ZoomOutButton = ControlsManager.GetMouseButtonAssociatedWithAction(PED_SNIPER_ZOOM_OUT); + // TODO: enum? this should be mouse wheel up and down + if(ZoomInButton == 4 || ZoomInButton == 5 || ZoomOutButton == 4 || ZoomOutButton == 5){ + if(CPad::GetPad(0)->GetMouseWheelUp() || CPad::GetPad(0)->GetMouseWheelDown()){ + if(CPad::GetPad(0)->SniperZoomIn()){ + TargetFOV = FOV - 10.0f; + UseMouse = true; + } + if(CPad::GetPad(0)->SniperZoomOut()){ + TargetFOV = FOV + 10.0f; + UseMouse = true; + } + } + } + if((CPad::GetPad(0)->SniperZoomIn() || CPad::GetPad(0)->SniperZoomOut()) && !UseMouse){ + if(CPad::GetPad(0)->SniperZoomOut()){ + FOV *= (255.0f*CTimer::GetTimeStep() + 10000.0f) / 10000.0f; + TargetFOV = FOV; + FOVSpeed = 0.0f; + }else{ + FOV /= (255.0f*CTimer::GetTimeStep() + 10000.0f) / 10000.0f; + TargetFOV = FOV; + FOVSpeed = 0.0f; + } + }else{ + if(Abs(TargetFOV - FOV) > 0.5f) + WellBufferMe(TargetFOV, &FOV, &FOVSpeed, 0.5f, 0.25f, false); + else + FOVSpeed = 0.0f; + } + + TheCamera.SetMotionBlur(180, 255, 180, 120, MBLUR_SNIPER); + + if(FOV > DefaultFOV) + FOV = DefaultFOV; + if(FOV < 15.0f) + FOV = 15.0f; + + Front = TargetCoors - Source; + Front.Normalise(); + Source += Front*0.4f; + + if(m_bCollisionChecksOn){ + if(!CWorld::GetIsLineOfSightClear(TargetCoors, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + CVector TestPoint; + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta + DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta + DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else{ + TestPoint.x = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Cos(Beta - DEGTORAD(35.0f)) + Source.x; + TestPoint.y = 3.0f * Cos(Alpha - DEGTORAD(20.0f)) * Sin(Beta - DEGTORAD(35.0f)) + Source.y; + TestPoint.z = 3.0f * Sin(Alpha - DEGTORAD(20.0f)) + Source.z; + if(!CWorld::GetIsLineOfSightClear(TestPoint, Source, true, true, false, true, false, true, true)){ + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + FailedTestTwelveFramesAgo = true; + }else + FailedTestTwelveFramesAgo = false; + } + } + } + + if(FailedTestTwelveFramesAgo) + RwCameraSetNearClipPlane(Scene.camera, 0.4f); + Source -= Front*0.4f; + + GetVectorsReadyForRW(); + float Rotation = CGeneral::GetATanOfXY(Front.x, Front.y) - HALFPI; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Rotation; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Rotation; +} + +void +CCam::Process_Syphon(const CVector &CameraTarget, float, float, float) +{ + FOV = DefaultFOV; + + if(!CamTargetEntity->IsPed()) + return; + + static bool CameraObscured = false; + // unused FailedClippingTestPrevously + static float BetaOffset = DEGTORAD(18.0f); + // unused AngleToGoTo + // unused AngleToGoToSpeed + // unused DistBetweenPedAndPlayerPreviouslyOn + static float HeightDown = -0.5f; + static float PreviousDistForInter; + CVector TargetCoors; + CVector2D vDist; + float fDist, fAimingDist; + float TargetAlpha; + CColPoint colPoint; + CEntity *entity; + + TargetCoors = CameraTarget; + + if(TheCamera.Cams[TheCamera.ActiveCam].Mode != MODE_SYPHON) + return; + + vDist = Source - TargetCoors; + fDist = vDist.Magnitude(); + if(fDist == 0.0f) + Source = TargetCoors + CVector(1.0f, 1.0f, 0.0f); + else + Source = TargetCoors + CVector(vDist.x/fDist * 1.7f, vDist.y/fDist * 1.7f, 0.0f); + if(fDist > 1.7f) + fDist = 1.7f; + + Beta = CGeneral::GetATanOfXY(Source.x - TargetCoors.x, Source.y - TargetCoors.y); + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + float NewBeta = CGeneral::GetATanOfXY(TheCamera.m_cvecAimingTargetCoors.x - TargetCoors.x, TheCamera.m_cvecAimingTargetCoors.y - TargetCoors.y) + PI; + if(ResetStatics){ + CameraObscured = false; + float TestBeta1 = NewBeta - BetaOffset - Beta; + float TestBeta2 = NewBeta + BetaOffset - Beta; + MakeAngleLessThan180(TestBeta1); + MakeAngleLessThan180(TestBeta2); + if(Abs(TestBeta1) < Abs(TestBeta2)) + BetaOffset = -BetaOffset; + // some unuseds + ResetStatics = false; + } + Beta = NewBeta + BetaOffset; + Source = TargetCoors; + Source.x += 1.7f*Cos(Beta); + Source.y += 1.7f*Sin(Beta); + TargetCoors.z += m_fSyphonModeTargetZOffSet; + fAimingDist = (TheCamera.m_cvecAimingTargetCoors - TargetCoors).Magnitude2D(); + if(fAimingDist < 6.5f) + fAimingDist = 6.5f; + TargetAlpha = CGeneral::GetATanOfXY(fAimingDist, TheCamera.m_cvecAimingTargetCoors.z - TargetCoors.z); + while(TargetAlpha >= PI) TargetAlpha -= 2*PI; + while(TargetAlpha < -PI) TargetAlpha += 2*PI; + + // inlined + WellBufferMe(-TargetAlpha, &Alpha, &AlphaSpeed, 0.07f, 0.015f, true); + + Source.z += fDist*Sin(Alpha) + fDist*0.2f; + if(Source.z < TargetCoors.z + HeightDown) + Source.z = TargetCoors.z + HeightDown; + + CameraObscured = CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true); + // PreviousDistForInter unused + if(CameraObscured){ + PreviousDistForInter = (TargetCoors - colPoint.point).Magnitude2D(); + Source = colPoint.point; + }else + PreviousDistForInter = 1.7f; + + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + m_fMinDistAwayFromCamWhenInterPolating = Front.Magnitude2D(); + if(m_fMinDistAwayFromCamWhenInterPolating < 1.1f) + RwCameraSetNearClipPlane(Scene.camera, max(m_fMinDistAwayFromCamWhenInterPolating - 0.35f, 0.05f)); + Front.Normalise(); + GetVectorsReadyForRW(); +} + +void +CCam::Process_Syphon_Crim_In_Front(const CVector &CameraTarget, float, float, float) +{ + FOV = DefaultFOV; + + if(!CamTargetEntity->IsPed()) + return; + + CVector TargetCoors = CameraTarget; + CVector vDist; + float fDist, TargetDist; + float zOffset; + float AimingAngle; + CColPoint colPoint; + CEntity *entity; + + TargetDist = TheCamera.m_fPedZoomValueSmooth * 0.5f + 4.0f; + vDist = Source - TargetCoors; + fDist = vDist.Magnitude2D(); + zOffset = TargetDist - 2.65f; + if(zOffset < 0.0f) + zOffset = 0.0f; + if(zOffset == 0.0f) + Source = TargetCoors + CVector(1.0f, 1.0f, zOffset); + else + Source = TargetCoors + CVector(vDist.x/fDist*TargetDist, vDist.y/fDist*TargetDist, zOffset); + + AimingAngle = CGeneral::GetATanOfXY(TheCamera.m_cvecAimingTargetCoors.x - TargetCoors.x, TheCamera.m_cvecAimingTargetCoors.y - TargetCoors.y); + while(AimingAngle >= PI) AimingAngle -= 2*PI; + while(AimingAngle < -PI) AimingAngle += 2*PI; + + if(TheCamera.PlayerWeaponMode.Mode == MODE_SYPHON) + Beta = AimingAngle + m_fPlayerInFrontSyphonAngleOffSet; + + Source.x = TargetCoors.x; + Source.y = TargetCoors.y; + Source.x += Cos(Beta) * TargetDist; + Source.y += Sin(Beta) * TargetDist; + + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true)){ + Beta = CGeneral::GetATanOfXY(Source.x - TargetCoors.x, Source.y - TargetCoors.y); + fDist = (TargetCoors - colPoint.point).Magnitude2D(); + Source.x = TargetCoors.x; + Source.y = TargetCoors.y; + Source.x += Cos(Beta) * fDist; + Source.y += Sin(Beta) * fDist; + } + + TargetCoors = CameraTarget; + TargetCoors.z += m_fSyphonModeTargetZOffSet; + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + GetVectorsReadyForRW(); +} + +void +CCam::Process_BehindBoat(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + if(!CamTargetEntity->IsVehicle()){ + ResetStatics = false; + return; + } + + CVector TargetCoors = CameraTarget; + float DeltaBeta = 0.0f; + static CColPoint colPoint; + CEntity *entity; + static float TargetWhenChecksWereOn = 0.0f; + static float CenterObscuredWhenChecksWereOn = 0.0f; + static float WaterZAddition = 2.75f; + float WaterLevel = 0.0f; + float s, c; + float Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); + FOV = DefaultFOV; + + if(ResetStatics){ + CenterObscuredWhenChecksWereOn = 0.0f; + TargetWhenChecksWereOn = 0.0f; + Beta = TargetOrientation + PI; + } + + CWaterLevel::GetWaterLevelNoWaves(TargetCoors.x, TargetCoors.y, TargetCoors.z, &WaterLevel); + WaterLevel += WaterZAddition; + static float FixerForGoingBelowGround = 0.4f; + if(-FixerForGoingBelowGround < TargetCoors.z-WaterLevel) + WaterLevel += TargetCoors.z-WaterLevel - FixerForGoingBelowGround; + + bool Obscured; + if(m_bCollisionChecksOn || ResetStatics){ + CVector TestPoint; + // Weird calculations here, also casting bool to float... + c = Cos(TargetOrientation); + s = Sin(TargetOrientation); + TestPoint = TheCamera.CarZoomValueSmooth * CVector(-c, -s, 0.0f) + + (TheCamera.CarZoomValueSmooth+7.0f) * CVector(-c, -s, 0.0f) + + TargetCoors; + TestPoint.z = WaterLevel + TheCamera.CarZoomValueSmooth; + float Test1 = CWorld::GetIsLineOfSightClear(TestPoint, TargetCoors, true, false, false, true, false, true, true); + + c = Cos(TargetOrientation + 0.8f); + s = Sin(TargetOrientation + DEGTORAD(40.0f)); + TestPoint = TheCamera.CarZoomValueSmooth * CVector(-c, -s, 0.0f) + + (TheCamera.CarZoomValueSmooth+7.0f) * CVector(-c, -s, 0.0f) + + TargetCoors; + TestPoint.z = WaterLevel + TheCamera.CarZoomValueSmooth; + float Test2 = CWorld::GetIsLineOfSightClear(TestPoint, TargetCoors, true, false, false, true, false, true, true); + + c = Cos(TargetOrientation - 0.8); + s = Sin(TargetOrientation - DEGTORAD(40.0f)); + TestPoint = TheCamera.CarZoomValueSmooth * CVector(-c, -s, 0.0f) + + (TheCamera.CarZoomValueSmooth+7.0f) * CVector(-c, -s, 0.0f) + + TargetCoors; + TestPoint.z = WaterLevel + TheCamera.CarZoomValueSmooth; + float Test3 = CWorld::GetIsLineOfSightClear(TestPoint, TargetCoors, true, false, false, true, false, true, true); + + if(Test2 == 0.0f){ + DeltaBeta = TargetOrientation - Beta - DEGTORAD(40.0f); + if(ResetStatics) + Beta = TargetOrientation - DEGTORAD(40.0f); + }else if(Test3 == 0.0f){ + DeltaBeta = TargetOrientation - Beta + DEGTORAD(40.0f); + if(ResetStatics) + Beta = TargetOrientation + DEGTORAD(40.0f); + }else if(Test1 == 0.0f){ + DeltaBeta = 0.0f; + }else if(Test2 != 0.0f && Test3 != 0.0f && Test1 != 0.0f){ + if(ResetStatics) + Beta = TargetOrientation; + DeltaBeta = TargetOrientation - Beta; + } + + c = Cos(Beta); + s = Sin(Beta); + TestPoint.x = TheCamera.CarZoomValueSmooth * -c + + (TheCamera.CarZoomValueSmooth + 7.0f) * -c + + TargetCoors.x; + TestPoint.y = TheCamera.CarZoomValueSmooth * -s + + (TheCamera.CarZoomValueSmooth + 7.0f) * -s + + TargetCoors.y; + TestPoint.z = WaterLevel + TheCamera.CarZoomValueSmooth; + Obscured = CWorld::ProcessLineOfSight(TestPoint, TargetCoors, colPoint, entity, true, false, false, true, false, true, true); + CenterObscuredWhenChecksWereOn = Obscured; + + // now DeltaBeta == TargetWhenChecksWereOn - Beta, which we need for WellBufferMe below + TargetWhenChecksWereOn = DeltaBeta + Beta; + }else{ + // DeltaBeta = TargetWhenChecksWereOn - Beta; // unneeded since we don't inline WellBufferMe + Obscured = CenterObscuredWhenChecksWereOn != 0.0f; + } + + if(Obscured){ + CWorld::ProcessLineOfSight(Source, TargetCoors, colPoint, entity, true, false, false, true, false, true, true); + Source = colPoint.point; + }else{ + // inlined + WellBufferMe(TargetWhenChecksWereOn, &Beta, &BetaSpeed, 0.07f, 0.015f, true); + + s = Sin(Beta); + c = Cos(Beta); + Source = TheCamera.CarZoomValueSmooth * CVector(-c, -s, 0.0f) + + (TheCamera.CarZoomValueSmooth+7.0f) * CVector(-c, -s, 0.0f) + + TargetCoors; + Source.z = WaterLevel + TheCamera.CarZoomValueSmooth; + } + + if(TheCamera.CarZoomValueSmooth < 0.05f){ + static float AmountUp = 2.2f; + TargetCoors.z += AmountUp * (0.0f - TheCamera.CarZoomValueSmooth); + } + TargetCoors.z += TheCamera.CarZoomValueSmooth + 0.5f; + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + GetVectorsReadyForRW(); + ResetStatics = false; +} + +void +CCam::Process_Fight_Cam(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + if(!CamTargetEntity->IsPed()) + return; + + FOV = DefaultFOV; + float BetaLeft, BetaRight, DeltaBetaLeft, DeltaBetaRight; + float BetaFix; + float Dist; + float BetaMaxSpeed = 0.015f; + float BetaAcceleration = 0.007f; + static bool PreviouslyFailedBuildingChecks = false; + float TargetCamHeight; + CVector TargetCoors; + + m_fMinDistAwayFromCamWhenInterPolating = 4.0f; + Front = Source - CameraTarget; + Beta = CGeneral::GetATanOfXY(Front.x, Front.y); + while(TargetOrientation >= PI) TargetOrientation -= 2*PI; + while(TargetOrientation < -PI) TargetOrientation += 2*PI; + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + // Figure out Beta + BetaLeft = TargetOrientation - HALFPI; + BetaRight = TargetOrientation + HALFPI; + DeltaBetaLeft = Beta - BetaLeft; + DeltaBetaRight = Beta - BetaRight; + while(DeltaBetaLeft >= PI) DeltaBetaLeft -= 2*PI; + while(DeltaBetaLeft < -PI) DeltaBetaLeft += 2*PI; + while(DeltaBetaRight >= PI) DeltaBetaRight -= 2*PI; + while(DeltaBetaRight < -PI) DeltaBetaRight += 2*PI; + + if(ResetStatics){ + if(Abs(DeltaBetaLeft) < Abs(DeltaBetaRight)) + m_fTargetBeta = DeltaBetaLeft; + else + m_fTargetBeta = DeltaBetaRight; + m_fBufferedTargetOrientation = TargetOrientation; + m_fBufferedTargetOrientationSpeed = 0.0f; + m_bCollisionChecksOn = true; + BetaSpeed = 0.0f; + }else if(CPad::GetPad(0)->WeaponJustDown()){ + if(Abs(DeltaBetaLeft) < Abs(DeltaBetaRight)) + m_fTargetBeta = DeltaBetaLeft; + else + m_fTargetBeta = DeltaBetaRight; + } + + // Check collisions + BetaFix = 0.0f; + Dist = Front.Magnitude2D(); + if(m_bCollisionChecksOn || PreviouslyFailedBuildingChecks){ + BetaFix = GetPedBetaAngleForClearView(CameraTarget, Dist+0.25f, 0.0f, true, false, false, true, false); + if(BetaFix == 0.0f){ + BetaFix = GetPedBetaAngleForClearView(CameraTarget, Dist+0.5f, DEGTORAD(24.0f), true, false, false, true, false); + if(BetaFix == 0.0f) + BetaFix = GetPedBetaAngleForClearView(CameraTarget, Dist+0.5f, -DEGTORAD(24.0f), true, false, false, true, false); + } + } + if(BetaFix != 0.0f){ + BetaMaxSpeed = 0.1f; + PreviouslyFailedBuildingChecks = true; + BetaAcceleration = 0.025f; + m_fTargetBeta = Beta + BetaFix; + } + WellBufferMe(m_fTargetBeta, &Beta, &BetaSpeed, BetaMaxSpeed, BetaAcceleration, true); + + Source = CameraTarget + 4.0f*CVector(Cos(Beta), Sin(Beta), 0.0f); + Source.z -= 0.5f; + + WellBufferMe(TargetOrientation, &m_fBufferedTargetOrientation, &m_fBufferedTargetOrientationSpeed, 0.07f, 0.004f, true); + TargetCoors = CameraTarget + 0.5f*CVector(Cos(m_fBufferedTargetOrientation), Sin(m_fBufferedTargetOrientation), 0.0f); + + TargetCamHeight = CameraTarget.z - Source.z + max(m_fPedBetweenCameraHeightOffset, m_fRoadOffSet + m_fDimensionOfHighestNearCar) - 0.5f; + if(TargetCamHeight > m_fCamBufferedHeight) + WellBufferMe(TargetCamHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.15f, 0.04f, false); + else + WellBufferMe(0.0f, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.08f, 0.0175f, false); + Source.z += m_fCamBufferedHeight; + + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + Front.Normalise(); + GetVectorsReadyForRW(); + + ResetStatics = false; +} + +/* +// Spline format is this, but game doesn't seem to use any kind of struct: +struct Spline +{ + float numFrames; + struct { + float time; + float f[3]; // CVector for Vector spline + } frames[1]; // numFrames +}; +*/ + +// These two functions are pretty ugly + +#define MS(t) (uint32)((t)*1000.0f) + +void +FindSplinePathPositionFloat(float *out, float *spline, uint32 time, uint32 &marker) +{ + // marker is at time + uint32 numFrames = spline[0]; + uint32 timeDelta = MS(spline[marker] - spline[marker-4]); + uint32 endTime = MS(spline[4*(numFrames-1) + 1]); + if(time < endTime){ + bool canAdvance = true; + if((marker-1)/4 > numFrames){ + canAdvance = false; + marker = 4*(numFrames-1) + 1; + } + // skipping over small time deltas apparently? + while(timeDelta <= 75 && canAdvance){ + marker += 4; + if((marker-1)/4 > numFrames){ + canAdvance = false; + marker = 4*(numFrames-1) + 1; + } + timeDelta = (spline[marker] - spline[marker-4]) * 1000.0f; + } + } + float a = ((float)time - (float)MS(spline[marker-4])) / (float)MS(spline[marker] - spline[marker-4]); + a = clamp(a, 0.0f, 1.0f); + float b = 1.0f - a; + *out = b*b*b * spline[marker-3] + + 3.0f*a*b*b * spline[marker-1] + + 3.0f*a*a*b * spline[marker+2] + + a*a*a * spline[marker+1]; +} + +void +FindSplinePathPositionVector(CVector *out, float *spline, uint32 time, uint32 &marker) +{ + // marker is at time + uint32 numFrames = spline[0]; + uint32 timeDelta = MS(spline[marker] - spline[marker-10]); + uint32 endTime = MS(spline[10*(numFrames-1) + 1]); + if(time < endTime){ + bool canAdvance = true; + if((marker-1)/10 > numFrames){ + canAdvance = false; + marker = 10*(numFrames-1) + 1; + } + // skipping over small time deltas apparently? + while(timeDelta <= 75 && canAdvance){ + marker += 10; + if((marker-1)/10 > numFrames){ + canAdvance = false; + marker = 10*(numFrames-1) + 1; + } + timeDelta = (spline[marker] - spline[marker-10]) * 1000.0f; + } + } + + if((marker-1)/10 > numFrames){ + printf("Arraymarker %i \n", marker); + printf("Path zero %i \n", numFrames); + } + + float a = ((float)time - (float)MS(spline[marker-10])) / (float)MS(spline[marker] - spline[marker-10]); + a = clamp(a, 0.0f, 1.0f); + float b = 1.0f - a; + out->x = + b*b*b * spline[marker-9] + + 3.0f*a*b*b * spline[marker-3] + + 3.0f*a*a*b * spline[marker+4] + + a*a*a * spline[marker+1]; + out->y = + b*b*b * spline[marker-8] + + 3.0f*a*b*b * spline[marker-2] + + 3.0f*a*a*b * spline[marker+5] + + a*a*a * spline[marker+2]; + out->z = + b*b*b * spline[marker-7] + + 3.0f*a*b*b * spline[marker-1] + + 3.0f*a*a*b * spline[marker+6] + + a*a*a * spline[marker+3]; + *out += TheCamera.m_vecCutSceneOffset; +} + +void +CCam::Process_FlyBy(const CVector&, float, float, float) +{ + float UpAngle = 0.0f; + static float FirstFOVValue = 0.0f; + static float PsuedoFOV; + static uint32 ArrayMarkerFOV; + static uint32 ArrayMarkerUp; + static uint32 ArrayMarkerSource; + static uint32 ArrayMarkerFront; + + if(TheCamera.m_bcutsceneFinished) + return; + + Up = CVector(0.0f, 0.0f, 1.0f); + if(TheCamera.m_bStartingSpline) + m_fTimeElapsedFloat += CTimer::GetTimeStepInMilliseconds(); + else{ + m_fTimeElapsedFloat = 0.0f; + m_uiFinishTime = MS(TheCamera.m_arrPathArray[2].m_arr_PathData[10*((int)TheCamera.m_arrPathArray[2].m_arr_PathData[0]-1) + 1]); + TheCamera.m_bStartingSpline = true; + FirstFOVValue = TheCamera.m_arrPathArray[0].m_arr_PathData[2]; + PsuedoFOV = TheCamera.m_arrPathArray[0].m_arr_PathData[2]; + ArrayMarkerFOV = 5; + ArrayMarkerUp = 5; + ArrayMarkerSource = 11; + ArrayMarkerFront = 11; + } + + float fTime = m_fTimeElapsedFloat; + uint32 uiFinishTime = m_uiFinishTime; + uint32 uiTime = fTime; + if(uiTime < uiFinishTime){ + TheCamera.m_fPositionAlongSpline = (float) uiTime / uiFinishTime; + + while(uiTime >= (TheCamera.m_arrPathArray[2].m_arr_PathData[ArrayMarkerSource] - TheCamera.m_arrPathArray[2].m_arr_PathData[1])*1000.0f) + ArrayMarkerSource += 10; + FindSplinePathPositionVector(&Source, TheCamera.m_arrPathArray[2].m_arr_PathData, uiTime, ArrayMarkerSource); + + while(uiTime >= (TheCamera.m_arrPathArray[3].m_arr_PathData[ArrayMarkerFront] - TheCamera.m_arrPathArray[3].m_arr_PathData[1])*1000.0f) + ArrayMarkerFront += 10; + FindSplinePathPositionVector(&Front, TheCamera.m_arrPathArray[3].m_arr_PathData, uiTime, ArrayMarkerFront); + + while(uiTime >= (TheCamera.m_arrPathArray[1].m_arr_PathData[ArrayMarkerUp] - TheCamera.m_arrPathArray[1].m_arr_PathData[1])*1000.0f) + ArrayMarkerUp += 4; + FindSplinePathPositionFloat(&UpAngle, TheCamera.m_arrPathArray[1].m_arr_PathData, uiTime, ArrayMarkerUp); + UpAngle = DEGTORAD(UpAngle) + HALFPI; + Up.x = Cos(UpAngle); + Up.z = Sin(UpAngle); + + while(uiTime >= (TheCamera.m_arrPathArray[0].m_arr_PathData[ArrayMarkerFOV] - TheCamera.m_arrPathArray[0].m_arr_PathData[1])*1000.0f) + ArrayMarkerFOV += 4; + FindSplinePathPositionFloat(&PsuedoFOV, TheCamera.m_arrPathArray[0].m_arr_PathData, uiTime, ArrayMarkerFOV); + + m_cvecTargetCoorsForFudgeInter = Front; + Front = Front - Source; + Front.Normalise(); + CVector Left = CrossProduct(Up, Front); + Up = CrossProduct(Front, Left); + Up.Normalise(); + FOV = PsuedoFOV; + }else{ + // end + ArrayMarkerSource = (TheCamera.m_arrPathArray[2].m_arr_PathData[0] - 1)*10 + 1; + ArrayMarkerFront = (TheCamera.m_arrPathArray[3].m_arr_PathData[0] - 1)*10 + 1; + ArrayMarkerUp = (TheCamera.m_arrPathArray[1].m_arr_PathData[0] - 1)*4 + 1; + ArrayMarkerFOV = (TheCamera.m_arrPathArray[0].m_arr_PathData[0] - 1)*4 + 1; + + FindSplinePathPositionVector(&Source, TheCamera.m_arrPathArray[2].m_arr_PathData, uiTime, ArrayMarkerSource); + FindSplinePathPositionVector(&Front, TheCamera.m_arrPathArray[3].m_arr_PathData, uiTime, ArrayMarkerFront); + FindSplinePathPositionFloat(&UpAngle, TheCamera.m_arrPathArray[1].m_arr_PathData, uiTime, ArrayMarkerUp); + UpAngle = DEGTORAD(UpAngle) + HALFPI; + Up.x = Cos(UpAngle); + Up.z = Sin(UpAngle); + FindSplinePathPositionFloat(&PsuedoFOV, TheCamera.m_arrPathArray[0].m_arr_PathData, uiTime, ArrayMarkerFOV); + + TheCamera.m_fPositionAlongSpline = 1.0f; + ArrayMarkerFOV = 0; + ArrayMarkerUp = 0; + ArrayMarkerSource = 0; + ArrayMarkerFront = 0; + + m_cvecTargetCoorsForFudgeInter = Front; + Front = Front - Source; + Front.Normalise(); + CVector Left = CrossProduct(Up, Front); + Up = CrossProduct(Front, Left); + Up.Normalise(); + FOV = PsuedoFOV; + } +} + +void +CCam::Process_WheelCam(const CVector&, float, float, float) +{ + FOV = DefaultFOV; + + if(CamTargetEntity->IsPed()){ + // what? ped with wheels or what? + Source = Multiply3x3(CamTargetEntity->GetMatrix(), CVector(-0.3f, -0.5f, 0.1f)); + Source += CamTargetEntity->GetPosition(); + Front = CVector(1.0f, 0.0f, 0.0f); + }else{ + Source = Multiply3x3(CamTargetEntity->GetMatrix(), CVector(-1.4f, -2.3f, 0.3f)); + Source += CamTargetEntity->GetPosition(); + Front = CamTargetEntity->GetForward(); + } + + CVector NewUp(0.0f, 0.0f, 1.0f); + CVector Left = CrossProduct(Front, NewUp); + Left.Normalise(); + NewUp = CrossProduct(Left, Front); + + float Roll = Cos((CTimer::GetTimeInMilliseconds()&0x1FFFF)/(float)0x1FFFF * TWOPI); + Up = Cos(Roll*0.4f)*NewUp + Sin(Roll*0.4f)*Left; +} + +void +CCam::Process_Fixed(const CVector &CameraTarget, float, float, float) +{ + Source = m_cvecCamFixedModeSource; + Front = CameraTarget - Source; + m_cvecTargetCoorsForFudgeInter = CameraTarget; + GetVectorsReadyForRW(); + + Up = CVector(0.0f, 0.0f, 1.0f) + m_cvecCamFixedModeUpOffSet; + Up.Normalise(); + CVector Right = CrossProduct(Front, Up); + Right.Normalise(); + Up = CrossProduct(Right, Front); + + FOV = DefaultFOV; + if(TheCamera.m_bUseSpecialFovTrain) + FOV = TheCamera.m_fFovForTrain; + + if(CMenuManager::m_ControlMethod == 0 && Using3rdPersonMouseCam()){ + CPed *player = FindPlayerPed(); + if(player && player->CanStrafeOrMouseControl()){ + float Heading = Front.Heading(); + ((CPed*)TheCamera.pTargetEntity)->m_fRotationCur = Heading; + ((CPed*)TheCamera.pTargetEntity)->m_fRotationDest = Heading; + TheCamera.pTargetEntity->SetHeading(Heading); + TheCamera.pTargetEntity->GetMatrix().UpdateRW(); + } + } +} + +void +CCam::Process_Player_Fallen_Water(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + CColPoint colPoint; + CEntity *entity = nil; + + FOV = DefaultFOV; + Source = CameraTarget; + Source.x += -4.5f*Cos(TargetOrientation); + Source.y += -4.5f*Sin(TargetOrientation); + Source.z = m_vecLastAboveWaterCamPosition.z + 4.0f; + + m_cvecTargetCoorsForFudgeInter = CameraTarget; + Front = CameraTarget - Source; + Front.Normalise(); + if(CWorld::ProcessLineOfSight(CameraTarget, Source, colPoint, entity, true, false, false, true, false, true, true)) + Source = colPoint.point; + GetVectorsReadyForRW(); + Front = CameraTarget - Source; + Front.Normalise(); +} + +// unused +void +CCam::Process_Circle(const CVector &CameraTarget, float, float, float) +{ + FOV = DefaultFOV; + + Front.x = Cos(0.7f) * Cos((CTimer::GetTimeInMilliseconds()&0xFFF)/(float)0xFFF * TWOPI); + Front.y = Cos(0.7f) * Sin((CTimer::GetTimeInMilliseconds()&0xFFF)/(float)0xFFF * TWOPI); + Front.z = -Sin(0.7f); + Source = CameraTarget - 4.0f*Front; + Source.z += 1.0f; + GetVectorsReadyForRW(); +} + +void +CCam::Process_SpecialFixedForSyphon(const CVector &CameraTarget, float, float, float) +{ + Source = m_cvecCamFixedModeSource; + m_cvecTargetCoorsForFudgeInter = CameraTarget; + m_cvecTargetCoorsForFudgeInter.z += m_fSyphonModeTargetZOffSet; + Front = CameraTarget - Source; + Front.z += m_fSyphonModeTargetZOffSet; + + GetVectorsReadyForRW(); + + Up += m_cvecCamFixedModeUpOffSet; + Up.Normalise(); + CVector Left = CrossProduct(Up, Front); + Left.Normalise(); + Front = CrossProduct(Left, Up); + Front.Normalise(); + FOV = DefaultFOV; +} + +#ifdef IMPROVED_CAMERA + +#define KEYJUSTDOWN(k) ControlsManager.GetIsKeyboardKeyJustDown((RsKeyCodes)k) +#define KEYDOWN(k) ControlsManager.GetIsKeyboardKeyDown((RsKeyCodes)k) +#define CTRLJUSTDOWN(key) \ + ((KEYDOWN(rsLCTRL) || KEYDOWN(rsRCTRL)) && KEYJUSTDOWN((RsKeyCodes)key) || \ + (KEYJUSTDOWN(rsLCTRL) || KEYJUSTDOWN(rsRCTRL)) && KEYDOWN((RsKeyCodes)key)) +#define CTRLDOWN(key) ((KEYDOWN(rsLCTRL) || KEYDOWN(rsRCTRL)) && KEYDOWN((RsKeyCodes)key)) + + +void +CCam::Process_Debug(const CVector&, float, float, float) +{ + static float Speed = 0.0f; + static float PanSpeedX = 0.0f; + static float PanSpeedY = 0.0f; + CVector TargetCoors; + + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + FOV = DefaultFOV; + Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; + Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; + if(CPad::GetPad(0)->GetLeftMouse()){ + Alpha += DEGTORAD(CPad::GetPad(0)->GetMouseY()/2.0f); + Beta -= DEGTORAD(CPad::GetPad(0)->GetMouseX()/2.0f); + } + + TargetCoors.x = Source.x + Cos(Alpha) * Cos(Beta) * 3.0f; + TargetCoors.y = Source.y + Cos(Alpha) * Sin(Beta) * 3.0f; + TargetCoors.z = Source.z + Sin(Alpha) * 3.0f; + + if(Alpha > DEGTORAD(89.5f)) Alpha = DEGTORAD(89.5f); + if(Alpha < DEGTORAD(-89.5f)) Alpha = DEGTORAD(-89.5f); + + if(CPad::GetPad(1)->GetSquare() || KEYDOWN('W')) + Speed += 0.1f; + else if(CPad::GetPad(1)->GetCross() || KEYDOWN('S')) + Speed -= 0.1f; + else + Speed = 0.0f; + if(Speed > 70.0f) Speed = 70.0f; + if(Speed < -70.0f) Speed = -70.0f; + + + if(KEYDOWN(rsRIGHT) || KEYDOWN('D')) + PanSpeedX += 0.1f; + else if(KEYDOWN(rsLEFT) || KEYDOWN('A')) + PanSpeedX -= 0.1f; + else + PanSpeedX = 0.0f; + if(PanSpeedX > 70.0f) PanSpeedX = 70.0f; + if(PanSpeedX < -70.0f) PanSpeedX = -70.0f; + + + if(KEYDOWN(rsUP)) + PanSpeedY += 0.1f; + else if(KEYDOWN(rsDOWN)) + PanSpeedY -= 0.1f; + 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; + + Up = CVector{ 0.0f, 0.0f, 1.0f }; + CVector Right = CrossProduct(Front, Up); + Up = CrossProduct(Right, Front); + Source = Source + Up*PanSpeedY + Right*PanSpeedX; + + if(Source.z < -450.0f) + Source.z = -450.0f; + + if(CPad::GetPad(1)->GetRightShoulder2JustDown() || KEYJUSTDOWN(rsENTER)){ + if(FindPlayerVehicle()) + FindPlayerVehicle()->Teleport(Source); + else + CWorld::Players[CWorld::PlayerInFocus].m_pPed->GetPosition() = Source; + } + + // stay inside sectors + while(CWorld::GetSectorX(Source.x) > 95.0f) + Source.x -= 1.0f; + while(CWorld::GetSectorX(Source.x) < 5.0f) + Source.x += 1.0f; + while(CWorld::GetSectorY(Source.y) > 95.0f) + Source.y -= 1.0f; + while(CWorld::GetSectorY(Source.y) < 5.0f) + Source.y += 1.0f; + GetVectorsReadyForRW(); + + CPad::GetPad(0)->DisablePlayerControls = PLAYERCONTROL_DISABLED_1; + + if(CPad::GetPad(1)->GetLeftShockJustDown() && gbBigWhiteDebugLightSwitchedOn) + CShadows::StoreShadowToBeRendered(SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &Source, + 12.0f, 0.0f, 0.0f, -12.0f, + 128, 128, 128, 128, 1000.0f, false, 1.0f); + + if(CHud::m_Wants_To_Draw_Hud){ + char str[256]; + sprintf(str, "CamX: %f CamY: %f CamZ: %f", Source.x, Source.y, Source.z); + sprintf(str, "Frontx: %f, Fronty: %f, Frontz: %f ", Front.x, Front.y, Front.z); + sprintf(str, "Look@: %f, Look@: %f, Look@: %f ", Front.x + Source.x, Front.y + Source.y, Front.z + Source.z); + } +} +#else +void +CCam::Process_Debug(const CVector&, float, float, float) +{ + static float Speed = 0.0f; + CVector TargetCoors; + + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + FOV = DefaultFOV; + Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; + Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; + + TargetCoors.x = Source.x + Cos(Alpha) * Cos(Beta) * 3.0f; + TargetCoors.y = Source.y + Cos(Alpha) * Sin(Beta) * 3.0f; + TargetCoors.z = Source.z + Sin(Alpha) * 3.0f; + + if(Alpha > DEGTORAD(89.5f)) Alpha = DEGTORAD(89.5f); + if(Alpha < DEGTORAD(-89.5f)) Alpha = DEGTORAD(-89.5f); + + if(CPad::GetPad(1)->GetSquare() || CPad::GetPad(1)->GetLeftMouse()) + Speed += 0.1f; + else if(CPad::GetPad(1)->GetCross() || CPad::GetPad(1)->GetRightMouse()) + Speed -= 0.1f; + else + Speed = 0.0f; + if(Speed > 70.0f) Speed = 70.0f; + if(Speed < -70.0f) Speed = -70.0f; + + Front = TargetCoors - Source; + Front.Normalise(); + Source = Source + Front*Speed; + + if(Source.z < -450.0f) + Source.z = -450.0f; + + if(CPad::GetPad(1)->GetRightShoulder2JustDown()){ + if(FindPlayerVehicle()) + FindPlayerVehicle()->Teleport(Source); + else + CWorld::Players[CWorld::PlayerInFocus].m_pPed->GetPosition() = Source; + + } + + // stay inside sectors + while(CWorld::GetSectorX(Source.x) > 95.0f) + Source.x -= 1.0f; + while(CWorld::GetSectorX(Source.x) < 5.0f) + Source.x += 1.0f; + while(CWorld::GetSectorY(Source.y) > 95.0f) + Source.y -= 1.0f; + while(CWorld::GetSectorY(Source.y) < 5.0f) + Source.y += 1.0f; + GetVectorsReadyForRW(); + + if(CPad::GetPad(1)->GetLeftShockJustDown() && gbBigWhiteDebugLightSwitchedOn) + CShadows::StoreShadowToBeRendered(SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &Source, + 12.0f, 0.0f, 0.0f, -12.0f, + 128, 128, 128, 128, 1000.0f, false, 1.0f); + + if(CHud::m_Wants_To_Draw_Hud){ + char str[256]; + sprintf(str, "CamX: %f CamY: %f CamZ: %f", Source.x, Source.y, Source.z); + sprintf(str, "Frontx: %f, Fronty: %f, Frontz: %f ", Front.x, Front.y, Front.z); + sprintf(str, "Look@: %f, Look@: %f, Look@: %f ", Front.x + Source.x, Front.y + Source.y, Front.z + Source.z); + } +} +#endif + +void +CCam::Process_Editor(const CVector&, float, float, float) +{ + static float Speed = 0.0f; + CVector TargetCoors; + + if(ResetStatics){ + Source = CVector(796.0f, -937.0, 40.0f); + CamTargetEntity = nil; + } + ResetStatics = false; + + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + FOV = DefaultFOV; + Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; + Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; + + if(CamTargetEntity && CSceneEdit::m_bCameraFollowActor){ + TargetCoors = CamTargetEntity->GetPosition(); + }else if(CSceneEdit::m_bRecording){ + TargetCoors.x = Source.x + Cos(Alpha) * Cos(Beta) * 7.0f; + TargetCoors.y = Source.y + Cos(Alpha) * Sin(Beta) * 7.0f; + TargetCoors.z = Source.z + Sin(Alpha) * 7.0f; + }else + TargetCoors = CSceneEdit::m_vecCamHeading + Source; + CSceneEdit::m_vecCurrentPosition = TargetCoors; + CSceneEdit::m_vecCamHeading = TargetCoors - Source; + + if(Alpha > DEGTORAD(89.5f)) Alpha = DEGTORAD(89.5f); + if(Alpha < DEGTORAD(-89.5f)) Alpha = DEGTORAD(-89.5f); + + if(CPad::GetPad(1)->GetSquare() || CPad::GetPad(1)->GetLeftMouse()) + Speed += 0.1f; + else if(CPad::GetPad(1)->GetCross() || CPad::GetPad(1)->GetRightMouse()) + Speed -= 0.1f; + else + Speed = 0.0f; + if(Speed > 70.0f) Speed = 70.0f; + if(Speed < -70.0f) Speed = -70.0f; + + Front = TargetCoors - Source; + Front.Normalise(); + Source = Source + Front*Speed; + + if(Source.z < -450.0f) + Source.z = -450.0f; + + if(CPad::GetPad(1)->GetRightShoulder2JustDown()){ + if(FindPlayerVehicle()) + FindPlayerVehicle()->Teleport(Source); + else + CWorld::Players[CWorld::PlayerInFocus].m_pPed->GetPosition() = Source; + + } + + // stay inside sectors + while(CWorld::GetSectorX(Source.x) > 95.0f) + Source.x -= 1.0f; + while(CWorld::GetSectorX(Source.x) < 5.0f) + Source.x += 1.0f; + while(CWorld::GetSectorY(Source.y) > 95.0f) + Source.y -= 1.0f; + while(CWorld::GetSectorY(Source.y) < 5.0f) + Source.y += 1.0f; + GetVectorsReadyForRW(); + + if(CPad::GetPad(1)->GetLeftShockJustDown() && gbBigWhiteDebugLightSwitchedOn) + CShadows::StoreShadowToBeRendered(SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &Source, + 12.0f, 0.0f, 0.0f, -12.0f, + 128, 128, 128, 128, 1000.0f, false, 1.0f); + + if(CHud::m_Wants_To_Draw_Hud){ + char str[256]; + sprintf(str, "CamX: %f CamY: %f CamZ: %f", Source.x, Source.y, Source.z); + sprintf(str, "Frontx: %f, Fronty: %f, Frontz: %f ", Front.x, Front.y, Front.z); + sprintf(str, "Look@: %f, Look@: %f, Look@: %f ", Front.x + Source.x, Front.y + Source.y, Front.z + Source.z); + } +} + +void +CCam::Process_ModelView(const CVector &CameraTarget, float, float, float) +{ + CVector TargetCoors = CameraTarget; + float Angle = Atan2(Front.x, Front.y); + FOV = DefaultFOV; + + Angle += CPad::GetPad(0)->GetLeftStickX()/1280.0f; + if(Distance < 10.0f) + Distance += CPad::GetPad(0)->GetLeftStickY()/1000.0f; + else + Distance += CPad::GetPad(0)->GetLeftStickY() * ((Distance - 10.0f)/20.0f + 1.0f) / 1000.0f; + if(Distance < 1.5f) + Distance = 1.5f; + + Front.x = Cos(0.3f) * Sin(Angle); + Front.y = Cos(0.3f) * Cos(Angle); + Front.z = -Sin(0.3f); + Source = CameraTarget - Distance*Front; + + GetVectorsReadyForRW(); +} + +void +CCam::ProcessPedsDeadBaby(void) +{ + float Distance = 0.0f; + static bool SafeToRotate = false; + CVector TargetDist, TestPoint; + + FOV = DefaultFOV; + TargetDist = Source - CamTargetEntity->GetPosition(); + Distance = TargetDist.Magnitude(); + Beta = CGeneral::GetATanOfXY(TargetDist.x, TargetDist.y); + while(Beta >= PI) Beta -= 2*PI; + while(Beta < -PI) Beta += 2*PI; + + if(ResetStatics){ + TestPoint = CamTargetEntity->GetPosition() + + CVector(4.0f * Cos(Alpha) * Cos(Beta), + 4.0f * Cos(Alpha) * Sin(Beta), + 4.0f * Sin(Alpha)); + bool Safe1 = CWorld::GetIsLineOfSightClear(TestPoint, CamTargetEntity->GetPosition(), true, false, false, true, false, true, true); + + TestPoint = CamTargetEntity->GetPosition() + + CVector(4.0f * Cos(Alpha) * Cos(Beta + DEGTORAD(120.0f)), + 4.0f * Cos(Alpha) * Sin(Beta + DEGTORAD(120.0f)), + 4.0f * Sin(Alpha)); + bool Safe2 = CWorld::GetIsLineOfSightClear(TestPoint, CamTargetEntity->GetPosition(), true, false, false, true, false, true, true); + + TestPoint = CamTargetEntity->GetPosition() + + CVector(4.0f * Cos(Alpha) * Cos(Beta - DEGTORAD(120.0f)), + 4.0f * Cos(Alpha) * Sin(Beta - DEGTORAD(120.0f)), + 4.0f * Sin(Alpha)); + bool Safe3 = CWorld::GetIsLineOfSightClear(TestPoint, CamTargetEntity->GetPosition(), true, false, false, true, false, true, true); + + SafeToRotate = Safe1 && Safe2 && Safe3; + + ResetStatics = false; + } + + if(SafeToRotate) + WellBufferMe(Beta + DEGTORAD(175.0f), &Beta, &BetaSpeed, 0.015f, 0.007f, true); + + WellBufferMe(DEGTORAD(89.5f), &Alpha, &AlphaSpeed, 0.015f, 0.07f, true); + WellBufferMe(35.0f, &Distance, &DistanceSpeed, 0.006f, 0.007f, false); + + Source = CamTargetEntity->GetPosition() + + CVector(Distance * Cos(Alpha) * Cos(Beta), + Distance * Cos(Alpha) * Sin(Beta), + Distance * Sin(Alpha)); + m_cvecTargetCoorsForFudgeInter = CamTargetEntity->GetPosition(); + Front = CamTargetEntity->GetPosition() - Source; + Front.Normalise(); + GetVectorsReadyForRW(); +} + +bool +CCam::ProcessArrestCamOne(void) +{ + FOV = 45.0f; + if(ResetStatics) + return true; + +#ifdef FIX_BUGS + if(!CamTargetEntity->IsPed()) + return true; +#endif + + bool found; + float Ground; + CVector PlayerCoors = TheCamera.pTargetEntity->GetPosition(); + CVector CopCoors = ((CPlayerPed*)TheCamera.pTargetEntity)->m_pArrestingCop->GetPosition(); + Beta = CGeneral::GetATanOfXY(PlayerCoors.x - CopCoors.x, PlayerCoors.y - CopCoors.y); + + Source = PlayerCoors + 9.5f*CVector(Cos(Beta), Sin(Beta), 0.0f); + Source.z += 6.0f; + Ground = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, Source.z, &found); + if(!found){ + Ground = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, Source.z, &found); + if(!found) + return false; + } + Source.z = Ground + 0.25f; + if(!CWorld::GetIsLineOfSightClear(Source, CopCoors, true, true, false, true, false, true, true)){ + Beta += DEGTORAD(115.0f); + Source = PlayerCoors + 9.5f*CVector(Cos(Beta), Sin(Beta), 0.0f); + Source.z += 6.0f; + Ground = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, Source.z, &found); + if(!found){ + Ground = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, Source.z, &found); + if(!found) + return false; + } + Source.z = Ground + 0.25f; + + CopCoors.z += 0.35f; + Front = CopCoors - Source; + if(!CWorld::GetIsLineOfSightClear(Source, CopCoors, true, true, false, true, false, true, true)) + return false; + } + CopCoors.z += 0.35f; + m_cvecTargetCoorsForFudgeInter = CopCoors; + Front = CopCoors - Source; + ResetStatics = false; + GetVectorsReadyForRW(); + return true; +} + +bool +CCam::ProcessArrestCamTwo(void) +{ + CPed *player = CWorld::Players[CWorld::PlayerInFocus].m_pPed; + if(!ResetStatics) + return true; + ResetStatics = false; + + CVector TargetCoors, ToCamera; + float BetaOffset; + float SourceX, SourceY; + CCam *ActiveCam = &TheCamera.Cams[TheCamera.ActiveCam]; + if(&ActiveCam[1] == this){ + SourceX = TheCamera.Cams[(TheCamera.ActiveCam + 1) % 2].Source.x; + SourceY = TheCamera.Cams[(TheCamera.ActiveCam + 1) % 2].Source.y; + }else{ + SourceX = ActiveCam[1].Source.x; + SourceY = ActiveCam[1].Source.y; + } + + for(int i = 0; i <= 1; i++){ + int Dir = i == 0 ? 1 : -1; + + TargetCoors = player->GetPosition(); + Beta = CGeneral::GetATanOfXY(TargetCoors.x-SourceX, TargetCoors.y-SourceY); + BetaOffset = DEGTORAD(Dir*80); + Source = TargetCoors + 11.5f*CVector(Cos(Beta+BetaOffset), Sin(Beta+BetaOffset), 0.0f); + + ToCamera = Source - TargetCoors; + ToCamera.Normalise(); + TargetCoors.x += 0.4f*ToCamera.x; + TargetCoors.y += 0.4f*ToCamera.y; + if(CWorld::GetIsLineOfSightClear(Source, TargetCoors, true, true, false, true, false, true, true)){ + Source.z += 5.5f; + TargetCoors += CVector(-0.8f*ToCamera.x, -0.8f*ToCamera.y, 2.2f); + m_cvecTargetCoorsForFudgeInter = TargetCoors; + Front = TargetCoors - Source; + ResetStatics = false; + GetVectorsReadyForRW(); + return true; + } + } + return false; +} + +STARTPATCHES + InjectHook(0x456F40, WellBufferMe, PATCH_JUMP); + InjectHook(0x458410, &CCam::Init, PATCH_JUMP); + InjectHook(0x4582F0, &CCam::GetVectorsReadyForRW, PATCH_JUMP); + InjectHook(0x457710, &CCam::DoAverageOnVector, PATCH_JUMP); + InjectHook(0x458060, &CCam::GetPedBetaAngleForClearView, PATCH_JUMP); + InjectHook(0x457210, &CCam::Cam_On_A_String_Unobscured, PATCH_JUMP); + InjectHook(0x457A80, &CCam::FixCamWhenObscuredByVehicle, PATCH_JUMP); + InjectHook(0x457B90, &CCam::FixCamIfObscured, PATCH_JUMP); + InjectHook(0x465DA0, &CCam::RotCamIfInFrontCar, PATCH_JUMP); + InjectHook(0x4662D0, &CCam::WorkOutCamHeightWeeCar, PATCH_JUMP); + InjectHook(0x466650, &CCam::WorkOutCamHeight, PATCH_JUMP); + InjectHook(0x458600, &CCam::LookBehind, PATCH_JUMP); + InjectHook(0x458C40, &CCam::LookLeft, PATCH_JUMP); + InjectHook(0x458FB0, &CCam::LookRight, PATCH_JUMP); + InjectHook(0x4574C0, &CCam::ClipIfPedInFrontOfPlayer, PATCH_JUMP); + InjectHook(0x459300, &CCam::KeepTrackOfTheSpeed, PATCH_JUMP); + InjectHook(0x458580, &CCam::IsTargetInWater, PATCH_JUMP); + InjectHook(0x4570C0, &CCam::AvoidWallsTopDownPed, PATCH_JUMP); + InjectHook(0x4595B0, &CCam::PrintMode, PATCH_JUMP); + + InjectHook(0x467400, &CCam::ProcessSpecialHeightRoutines, PATCH_JUMP); + InjectHook(0x4596A0, &CCam::Process, PATCH_JUMP); + InjectHook(0x45E3A0, &CCam::Process_FollowPed, PATCH_JUMP); + InjectHook(0x45FF70, &CCam::Process_FollowPedWithMouse, PATCH_JUMP); + InjectHook(0x45BE60, &CCam::Process_BehindCar, PATCH_JUMP); + InjectHook(0x45C090, &CCam::Process_Cam_On_A_String, PATCH_JUMP); + InjectHook(0x463EB0, &CCam::Process_TopDown, PATCH_JUMP); + InjectHook(0x464390, &CCam::Process_TopDownPed, PATCH_JUMP); + InjectHook(0x461AF0, &CCam::Process_Rocket, PATCH_JUMP); + InjectHook(0x460E00, &CCam::Process_M16_1stPerson, PATCH_JUMP); + InjectHook(0x459FA0, &CCam::Process_1stPerson, PATCH_JUMP); + InjectHook(0x462420, &CCam::Process_Sniper, PATCH_JUMP); + InjectHook(0x463130, &CCam::Process_Syphon, PATCH_JUMP); + InjectHook(0x463A70, &CCam::Process_Syphon_Crim_In_Front, PATCH_JUMP); + InjectHook(0x45B470, &CCam::Process_BehindBoat, PATCH_JUMP); + InjectHook(0x45D2F0, &CCam::Process_Fight_Cam, PATCH_JUMP); + InjectHook(0x45DC20, &CCam::Process_FlyBy, PATCH_JUMP); + InjectHook(0x464D10, &CCam::Process_WheelCam, PATCH_JUMP); + InjectHook(0x45DA20, &CCam::Process_Fixed, PATCH_JUMP); + InjectHook(0x461940, &CCam::Process_Player_Fallen_Water, PATCH_JUMP); + InjectHook(0x45C400, &CCam::Process_Circle, PATCH_JUMP); + InjectHook(0x462FC0, &CCam::Process_SpecialFixedForSyphon, PATCH_JUMP); + InjectHook(0x45CCC0, &CCam::Process_Debug, PATCH_JUMP); + InjectHook(0x4656C0, &CCam::ProcessPedsDeadBaby, PATCH_JUMP); + InjectHook(0x465000, &CCam::ProcessArrestCamOne, PATCH_JUMP); + InjectHook(0x4653C0, &CCam::ProcessArrestCamTwo, PATCH_JUMP); + + InjectHook(0x456CE0, &FindSplinePathPositionFloat, PATCH_JUMP); + InjectHook(0x4569A0, &FindSplinePathPositionVector, PATCH_JUMP); + + InjectHook(0x473250, &CCamera::dtor, PATCH_JUMP); +ENDPATCHES diff --git a/src/core/Camera.cpp b/src/core/Camera.cpp index 75e52c5f..3f7ed286 100644 --- a/src/core/Camera.cpp +++ b/src/core/Camera.cpp @@ -13,8 +13,6 @@ #include "MBlur.h" #include "Camera.h" -const float DefaultFOV = 70.0f; // beta: 80.0f - CCamera &TheCamera = *(CCamera*)0x6FACF8; bool &CCamera::m_bUseMouse3rdPerson = *(bool *)0x5F03D8; @@ -38,6 +36,15 @@ CCamera::GetFading() return m_bFading; } +int +CCamera::GetFadingDirection() +{ + if(m_bFading) + return m_iFadingDirection == FADE_IN ? FADE_IN : FADE_OUT; + else + return FADE_NONE; +} + bool CCamera::IsSphereVisible(const CVector ¢er, float radius, const CMatrix *mat) { @@ -182,1148 +189,6 @@ CCamera::ClearPlayerWeaponMode() PlayerWeaponMode.Duration = 0.0f; } - -/* - * - * CCam - * - */ - - -// MaxSpeed is a limit of how fast the value is allowed to change. 1.0 = to Target in up to 1ms -// Acceleration is how fast the speed will change to MaxSpeed. 1.0 = to MaxSpeed in 1ms -void -WellBufferMe(float Target, float *CurrentValue, float *CurrentSpeed, float MaxSpeed, float Acceleration, bool IsAngle) -{ - float Delta = Target - *CurrentValue; - - if(IsAngle){ - while(Delta >= PI) Delta -= 2*PI; - while(Delta < -PI) Delta += 2*PI; - } - - float TargetSpeed = Delta * MaxSpeed; - // Add or subtract absolute depending on sign, genius! -// if(TargetSpeed - *CurrentSpeed > 0.0f) -// *CurrentSpeed += Acceleration * Abs(TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); -// else -// *CurrentSpeed -= Acceleration * Abs(TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); - // this is simpler: - *CurrentSpeed += Acceleration * (TargetSpeed - *CurrentSpeed) * CTimer::GetTimeStep(); - - // Clamp speed if we overshot - if(TargetSpeed < 0.0f && *CurrentSpeed < TargetSpeed) - *CurrentSpeed = TargetSpeed; - else if(TargetSpeed > 0.0f && *CurrentSpeed > TargetSpeed) - *CurrentSpeed = TargetSpeed; - - *CurrentValue += *CurrentSpeed * min(10.0f, CTimer::GetTimeStep()); -} - -void -CCam::GetVectorsReadyForRW(void) -{ - CVector right; - Up = CVector(0.0f, 0.0f, 1.0f); - Front.Normalise(); - if(Front.x == 0.0f && Front.y == 0.0f){ - Front.x = 0.0001f; - Front.y = 0.0001f; - } - right = CrossProduct(Front, Up); - right.Normalise(); - Up = CrossProduct(right, Front); -} - -// This code is really bad. wtf R*? -CVector -CCam::DoAverageOnVector(const CVector &vec) -{ - int i; - CVector Average = { 0.0f, 0.0f, 0.0f }; - - if(ResetStatics){ - m_iRunningVectorArrayPos = 0; - m_iRunningVectorCounter = 1; - } - - // TODO: make this work with NUMBER_OF_VECTORS_FOR_AVERAGE != 2 - if(m_iRunningVectorCounter == 3){ - m_arrPreviousVectors[0] = m_arrPreviousVectors[1]; - m_arrPreviousVectors[1] = vec; - }else - m_arrPreviousVectors[m_iRunningVectorArrayPos] = vec; - - for(i = 0; i <= m_iRunningVectorArrayPos; i++) - Average += m_arrPreviousVectors[i]; - Average /= i; - - m_iRunningVectorArrayPos++; - m_iRunningVectorCounter++; - if(m_iRunningVectorArrayPos >= NUMBER_OF_VECTORS_FOR_AVERAGE) - m_iRunningVectorArrayPos = NUMBER_OF_VECTORS_FOR_AVERAGE-1; - if(m_iRunningVectorCounter > NUMBER_OF_VECTORS_FOR_AVERAGE+1) - m_iRunningVectorCounter = NUMBER_OF_VECTORS_FOR_AVERAGE+1; - - return Average; -} - -// Rotate Beta in direction opposite of BetaOffset in 5 deg. steps. -// Return the first angle for which Beta + BetaOffset + Angle has a clear view. -// i.e. BetaOffset is a safe zone so that Beta + Angle is really clear. -// If BetaOffset == 0, try both directions. -float -CCam::GetPedBetaAngleForClearView(const CVector &Target, float Dist, float BetaOffset, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies) -{ - CColPoint point; - CEntity *ent = nil; - CVector ToSource; - float a; - - // This would be so much nicer if we just got the step variable before the loop...R* - - for(a = 0.0f; a <= PI; a += DEGTORAD(5.0f)){ - if(BetaOffset <= 0.0f){ - ToSource = CVector(Cos(Beta + BetaOffset + a), Sin(Beta + BetaOffset + a), 0.0f)*Dist; - if(!CWorld::ProcessLineOfSight(Target, Target + ToSource, - point, ent, checkBuildings, checkVehicles, checkPeds, - checkObjects, checkDummies, true, true)) - return a; - } - if(BetaOffset >= 0.0f){ - ToSource = CVector(Cos(Beta + BetaOffset - a), Sin(Beta + BetaOffset - a), 0.0f)*Dist; - if(!CWorld::ProcessLineOfSight(Target, Target + ToSource, - point, ent, checkBuildings, checkVehicles, checkPeds, - checkObjects, checkDummies, true, true)) - return -a; - } - } - return 0.0f; -} - -static float DefaultAcceleration = 0.045f; -static float DefaultMaxStep = 0.15f; - -void -CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, float, float) -{ - const float GroundDist = 1.85f; - - CVector TargetCoors, Dist, IdealSource; - float Length = 0.0f; - float LateralLeft = 0.0f; - float LateralRight = 0.0f; - float Center = 0.0f; - static bool PreviouslyObscured; - static bool PickedASide; - static float FixedTargetOrientation = 0.0f; - float AngleToGoTo = 0.0f; - float BetaOffsetAvoidBuildings = 0.45f; // ~25 deg - float BetaOffsetGoingBehind = 0.45f; - bool GoingBehind = false; - bool Obscured = false; - bool BuildingCheckObscured = false; - bool HackPlayerOnStoppingTrain = false; - static int TimeIndicatedWantedToGoDown = 0; - static bool StartedCountingForGoDown = false; - float DeltaBeta; - - m_bFixingBeta = false; - bBelowMinDist = false; - bBehindPlayerDesired = false; - - assert(CamTargetEntity->IsPed()); - - // CenterDist should be > LateralDist because we don't have an angle for safety in this case - float CenterDist, LateralDist; - float AngleToGoToSpeed; - if(m_fCloseInPedHeightOffsetSpeed > 0.00001f){ - LateralDist = 0.55f; - CenterDist = 1.25f; - BetaOffsetAvoidBuildings = 0.9f; // ~50 deg - BetaOffsetGoingBehind = 0.9f; - AngleToGoToSpeed = 0.88254666f; - }else{ - LateralDist = 0.8f; - CenterDist = 1.35f; - if(TheCamera.PedZoomIndicator == 1.0f || TheCamera.PedZoomIndicator == 4.0f){ - LateralDist = 1.25f; - CenterDist = 1.6f; - } - AngleToGoToSpeed = 0.43254671f; - } - - FOV = DefaultFOV; - - if(ResetStatics){ - Rotating = false; - m_bCollisionChecksOn = true; - FixedTargetOrientation = 0.0f; - PreviouslyObscured = false; - PickedASide = false; - StartedCountingForGoDown = false; - AngleToGoTo = 0.0f; - // unused LastAngleWithNoPickedASide - } - - - TargetCoors = CameraTarget; - IdealSource = Source; - TargetCoors.z += m_fSyphonModeTargetZOffSet; - - CVector TempTargetCoors; - TempTargetCoors = DoAverageOnVector(TargetCoors); - TargetCoors = TempTargetCoors; - // Add this unknown offset, but later it's removed again - TargetCoors.z += m_fUnknownZOffSet; - - Dist.x = IdealSource.x - TargetCoors.x; - Dist.y = IdealSource.y - TargetCoors.y; - Length = Dist.Magnitude2D(); - - // Cam on a string. With a fixed distance. Zoom in/out is done later. - if(Length != 0.0f) - IdealSource = TargetCoors + CVector(Dist.x, Dist.y, 0.0f)/Length * GroundDist; - else - IdealSource = TargetCoors + CVector(1.0f, 1.0f, 0.0f); - - // TODO: what's transition beta? - if(TheCamera.m_bUseTransitionBeta && ResetStatics){ - CVector VecDistance; - IdealSource.x = TargetCoors.x + GroundDist*Cos(m_fTransitionBeta); - IdealSource.y = TargetCoors.y + GroundDist*Sin(m_fTransitionBeta); - Beta = CGeneral::GetATanOfXY(IdealSource.x - TargetCoors.x, IdealSource.y - TargetCoors.y); - }else - Beta = CGeneral::GetATanOfXY(Source.x - TargetCoors.x, Source.y - TargetCoors.y); - - if(TheCamera.m_bCamDirectlyBehind){ - m_bCollisionChecksOn = true; - Beta = TargetOrientation + PI; - } - - if(FindPlayerVehicle()) - if(FindPlayerVehicle()->m_vehType == VEHICLE_TYPE_TRAIN) - HackPlayerOnStoppingTrain = true; - - if(TheCamera.m_bCamDirectlyInFront){ - m_bCollisionChecksOn = true; - Beta = TargetOrientation; - } - - while(Beta >= PI) Beta -= 2.0f * PI; - while(Beta < -PI) Beta += 2.0f * PI; - - // BUG? is this ever used? - // The values seem to be roughly m_fPedZoomValueSmooth + 1.85 - if(ResetStatics){ - if(TheCamera.PedZoomIndicator == 1.0f) m_fRealGroundDist = 2.090556f; - if(TheCamera.PedZoomIndicator == 2.0f) m_fRealGroundDist = 3.34973f; - if(TheCamera.PedZoomIndicator == 3.0f) m_fRealGroundDist = 4.704914f; - if(TheCamera.PedZoomIndicator == 4.0f) m_fRealGroundDist = 2.090556f; - } - // And what is this? It's only used for collision and rotation it seems - float RealGroundDist; - if(TheCamera.PedZoomIndicator == 1.0f) RealGroundDist = 2.090556f; - if(TheCamera.PedZoomIndicator == 2.0f) RealGroundDist = 3.34973f; - if(TheCamera.PedZoomIndicator == 3.0f) RealGroundDist = 4.704914f; - if(TheCamera.PedZoomIndicator == 4.0f) RealGroundDist = 2.090556f; - if(m_fCloseInPedHeightOffset > 0.00001f) - RealGroundDist = 1.7016f; - - - bool Shooting = false; - CPed *ped = (CPed*)CamTargetEntity; - if(ped->GetWeapon()->m_eWeaponType != WEAPONTYPE_UNARMED) - if(CPad::GetPad(0)->GetWeapon()) - Shooting = true; - if(ped->GetWeapon()->m_eWeaponType == WEAPONTYPE_DETONATOR || - ped->GetWeapon()->m_eWeaponType == WEAPONTYPE_BASEBALLBAT) - Shooting = false; - - - if(m_fCloseInPedHeightOffset > 0.00001f) - TargetCoors.z -= m_fUnknownZOffSet; - - // Figure out if and where we want to rotate - - if(CPad::GetPad(0)->ForceCameraBehindPlayer() || Shooting){ - - // Center cam behind player - - GoingBehind = true; - m_bCollisionChecksOn = true; - float OriginalBeta = Beta; - // Set Beta behind player - Beta = TargetOrientation + PI; - TargetCoors.z -= 0.1f; - - AngleToGoTo = GetPedBetaAngleForClearView(TargetCoors, CenterDist * RealGroundDist, 0.0f, true, false, false, true, false); - if(AngleToGoTo != 0.0f){ - if(AngleToGoTo < 0.0f) - AngleToGoTo -= AngleToGoToSpeed; - else - AngleToGoTo += AngleToGoToSpeed; - }else{ - float LateralLeft = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, BetaOffsetGoingBehind, true, false, false, true, false); - float LateralRight = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, -BetaOffsetGoingBehind, true, false, false, true, false); - if(LateralLeft == 0.0f && LateralRight != 0.0f) - AngleToGoTo += LateralRight; - else if(LateralLeft != 0.0f && LateralRight == 0.0f) - AngleToGoTo += LateralLeft; - } - - TargetCoors.z += 0.1f; - Beta = OriginalBeta; - - if(PickedASide){ - if(AngleToGoTo == 0.0f) - FixedTargetOrientation = TargetOrientation + PI; - Rotating = true; - }else{ - FixedTargetOrientation = TargetOrientation + PI + AngleToGoTo; - Rotating = true; - PickedASide = true; - } - }else{ - - // Rotate cam to avoid clipping into buildings - - TargetCoors.z -= 0.1f; - - Center = GetPedBetaAngleForClearView(TargetCoors, CenterDist * RealGroundDist, 0.0f, true, false, false, true, false); - if(m_bCollisionChecksOn || PreviouslyObscured || Center != 0.0f || m_fCloseInPedHeightOffset > 0.00001f){ - if(Center != 0.0f){ - AngleToGoTo = Center; - }else{ - LateralLeft = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, BetaOffsetAvoidBuildings, true, false, false, true, false); - LateralRight = GetPedBetaAngleForClearView(TargetCoors, LateralDist * RealGroundDist, -BetaOffsetAvoidBuildings, true, false, false, true, false); - if(LateralLeft == 0.0f && LateralRight != 0.0f){ - AngleToGoTo += LateralRight; - if(m_fCloseInPedHeightOffset > 0.0f) - RwCameraSetNearClipPlane(Scene.camera, 0.7f); - }else if(LateralLeft != 0.0f && LateralRight == 0.0f){ - AngleToGoTo += LateralLeft; - if(m_fCloseInPedHeightOffset > 0.0f) - RwCameraSetNearClipPlane(Scene.camera, 0.7f); - } - } - if(LateralLeft != 0.0f || LateralRight != 0.0f || Center != 0.0f) - BuildingCheckObscured = true; - } - - TargetCoors.z += 0.1f; - } - - if(m_fCloseInPedHeightOffset > 0.00001f) - TargetCoors.z += m_fUnknownZOffSet; - - - // Have to fix to avoid collision - - if(AngleToGoTo != 0.0f){ - Obscured = true; - Rotating = true; - if(CPad::GetPad(0)->ForceCameraBehindPlayer() || Shooting){ - if(!PickedASide) - FixedTargetOrientation = Beta + AngleToGoTo; // can this even happen? - }else - FixedTargetOrientation = Beta + AngleToGoTo; - - // This calculation is only really used to figure out how fast to rotate out of collision - - m_fAmountFractionObscured = 1.0f; - CVector PlayerPos = FindPlayerPed()->GetPosition(); - float RotationDist = (AngleToGoTo == Center ? CenterDist : LateralDist) * RealGroundDist; - // What's going on here? - AngleToGoTo? - CVector RotatedSource = PlayerPos + CVector(Cos(Beta - AngleToGoTo), Sin(Beta - AngleToGoTo), 0.0f) * RotationDist; - - CColPoint colpoint; - CEntity *entity; - if(CWorld::ProcessLineOfSight(PlayerPos, RotatedSource, colpoint, entity, true, false, false, true, false, false, false)){ - if((PlayerPos - RotatedSource).Magnitude() != 0.0f) - m_fAmountFractionObscured = (PlayerPos - colpoint.point).Magnitude() / (PlayerPos - RotatedSource).Magnitude(); - else - m_fAmountFractionObscured = 1.0f; - } - } - if(m_fAmountFractionObscured < 0.0f) m_fAmountFractionObscured = 0.0f; - if(m_fAmountFractionObscured > 1.0f) m_fAmountFractionObscured = 1.0f; - - - - // Figure out speed values for Beta rotation - - float Acceleration, MaxSpeed; - static float AccelerationMult = 0.35f; - static float MaxSpeedMult = 0.85f; - static float AccelerationMultClose = 0.7f; - static float MaxSpeedMultClose = 1.6f; - float BaseAcceleration = 0.025f; - float BaseMaxSpeed = 0.09f; - if(m_fCloseInPedHeightOffset > 0.00001f){ - if(AngleToGoTo == 0.0f){ - BaseAcceleration = 0.022f; - BaseMaxSpeed = 0.04f; - }else{ - BaseAcceleration = DefaultAcceleration; - BaseMaxSpeed = DefaultMaxStep; - } - } - if(AngleToGoTo == 0.0f){ - Acceleration = BaseAcceleration; - MaxSpeed = BaseMaxSpeed; - }else if(CPad::GetPad(0)->ForceCameraBehindPlayer() && !Shooting){ - Acceleration = 0.051f; - MaxSpeed = 0.18f; - }else if(m_fCloseInPedHeightOffset > 0.00001f){ - Acceleration = BaseAcceleration + AccelerationMultClose*sq(m_fAmountFractionObscured - 1.05f); - MaxSpeed = BaseMaxSpeed + MaxSpeedMultClose*sq(m_fAmountFractionObscured - 1.05f); - }else{ - Acceleration = DefaultAcceleration + AccelerationMult*sq(m_fAmountFractionObscured - 1.05f); - MaxSpeed = DefaultMaxStep + MaxSpeedMult*sq(m_fAmountFractionObscured - 1.05f); - } - static float AccelerationLimit = 0.3f; - static float MaxSpeedLimit = 0.65f; - if(Acceleration > AccelerationLimit) Acceleration = AccelerationLimit; - if(MaxSpeed > MaxSpeedLimit) MaxSpeed = MaxSpeedLimit; - - - int MoveState = ((CPed*)CamTargetEntity)->m_nMoveState; - if(MoveState != PEDMOVE_NONE && MoveState != PEDMOVE_STILL && - !CPad::GetPad(0)->ForceCameraBehindPlayer() && !Obscured && !Shooting){ - Rotating = false; - BetaSpeed = 0.0f; - } - - // Now do the Beta rotation - - float Distance = (IdealSource - TargetCoors).Magnitude2D(); - m_fDistanceBeforeChanges = Distance; - - if(Rotating){ - m_bFixingBeta = true; - - while(FixedTargetOrientation >= PI) FixedTargetOrientation -= 2*PI; - while(FixedTargetOrientation < -PI) FixedTargetOrientation += 2*PI; - - while(Beta >= PI) Beta -= 2*PI; - while(Beta < -PI) Beta += 2*PI; - - -/* - // This is inlined WellBufferMe - DeltaBeta = FixedTargetOrientation - Beta; - while(DeltaBeta >= PI) DeltaBeta -= 2*PI; - while(DeltaBeta < -PI) DeltaBeta += 2*PI; - - float ReqSpeed = DeltaBeta * MaxSpeed; - // Add or subtract absolute depending on sign, genius! - if(ReqSpeed - BetaSpeed > 0.0f) - BetaSpeed += SpeedStep * Abs(ReqSpeed - BetaSpeed) * CTimer::GetTimeStep(); - else - BetaSpeed -= SpeedStep * Abs(ReqSpeed - BetaSpeed) * CTimer::GetTimeStep(); - // this would be simpler: - // BetaSpeed += SpeedStep * (ReqSpeed - BetaSpeed) * CTimer::ms_fTimeStep; - - if(ReqSpeed < 0.0f && BetaSpeed < ReqSpeed) - BetaSpeed = ReqSpeed; - else if(ReqSpeed > 0.0f && BetaSpeed > ReqSpeed) - BetaSpeed = ReqSpeed; - - Beta += BetaSpeed * min(10.0f, CTimer::GetTimeStep()); -*/ - WellBufferMe(FixedTargetOrientation, &Beta, &BetaSpeed, MaxSpeed, Acceleration, true); - - if(ResetStatics){ - Beta = FixedTargetOrientation; - BetaSpeed = 0.0f; - } - - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); - - // Check if we can stop rotating - DeltaBeta = FixedTargetOrientation - Beta; - while(DeltaBeta >= PI) DeltaBeta -= 2*PI; - while(DeltaBeta < -PI) DeltaBeta += 2*PI; - if(Abs(DeltaBeta) < DEGTORAD(1.0f) && !bBehindPlayerDesired){ - // Stop rotation - PickedASide = false; - Rotating = false; - BetaSpeed = 0.0f; - } - } - - - if(TheCamera.m_bCamDirectlyBehind || TheCamera.m_bCamDirectlyInFront || - HackPlayerOnStoppingTrain || Rotating){ - if(TheCamera.m_bCamDirectlyBehind){ - Beta = TargetOrientation + PI; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); - } - if(TheCamera.m_bCamDirectlyInFront){ - Beta = TargetOrientation; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); - } - if(HackPlayerOnStoppingTrain){ - Beta = TargetOrientation + PI; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); - m_fDimensionOfHighestNearCar = 0.0f; - m_fCamBufferedHeight = 0.0f; - m_fCamBufferedHeightSpeed = 0.0f; - } - // Beta and Source already set in the rotation code - }else{ - Source = IdealSource; - BetaSpeed = 0.0f; - } - - // Subtract m_fUnknownZOffSet from both? - TargetCoors.z -= m_fUnknownZOffSet; - Source.z = IdealSource.z - m_fUnknownZOffSet; - - // Apply zoom now - // m_fPedZoomValueSmooth makes the cam go down the further out it is - // 0.25 -> 0.20 for nearest dist - // 1.50 -> -0.05 for mid dist - // 2.90 -> -0.33 for far dist - Source.z += (2.5f - TheCamera.m_fPedZoomValueSmooth)*0.2f - 0.25f; - // Zoom out camera - Front = TargetCoors - Source; - Front.Normalise(); - Source -= Front * TheCamera.m_fPedZoomValueSmooth; - // and then we move up again - // -0.375 - // 0.25 - // 0.95 - Source.z += (TheCamera.m_fPedZoomValueSmooth - 1.0f)*0.5f + m_fCloseInPedHeightOffset; - - - // Process height offset to avoid peds and cars - - float TargetZOffSet = m_fUnknownZOffSet + m_fDimensionOfHighestNearCar; - TargetZOffSet = max(TargetZOffSet, m_fPedBetweenCameraHeightOffset); - float TargetHeight = CameraTarget.z + TargetZOffSet - Source.z; - - if(TargetHeight > m_fCamBufferedHeight){ - // Have to go up - if(TargetZOffSet == m_fPedBetweenCameraHeightOffset && TargetZOffSet > m_fCamBufferedHeight) - WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.04f, false); - else if(TargetZOffSet == m_fUnknownZOffSet && TargetZOffSet > m_fCamBufferedHeight){ - // TODO: figure this out - bool foo = false; - switch(((CPhysical*)CamTargetEntity)->m_nSurfaceTouched) - case SURFACE_GRASS: - case SURFACE_DIRT: - case SURFACE_PAVEMENT: - case SURFACE_STEEL: - case SURFACE_TIRE: - case SURFACE_STONE: - foo = true; - if(foo) - WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.4f, 0.05f, false); - else - WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.025f, false); - }else - WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.025f, false); - StartedCountingForGoDown = false; - }else{ - // Have to go down - if(StartedCountingForGoDown){ - if(CTimer::GetTimeInMilliseconds() != TimeIndicatedWantedToGoDown){ - if(TargetHeight > 0.0f) - WellBufferMe(TargetHeight, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.01f, false); - else - WellBufferMe(0.0f, &m_fCamBufferedHeight, &m_fCamBufferedHeightSpeed, 0.2f, 0.01f, false); - } - }else{ - StartedCountingForGoDown = true; - TimeIndicatedWantedToGoDown = CTimer::GetTimeInMilliseconds(); - } - } - - Source.z += m_fCamBufferedHeight; - - - // Clip Source if necessary - - bool ClipSource = m_fCloseInPedHeightOffset > 0.00001f && m_fCamBufferedHeight > 0.001f; - if(GoingBehind || ResetStatics || ClipSource){ - CColPoint colpoint; - CEntity *entity; - if(CWorld::ProcessLineOfSight(TargetCoors, Source, colpoint, entity, true, false, false, true, false, true, true)){ - Source = colpoint.point; - if((TargetCoors - Source).Magnitude2D() < 1.0f) - RwCameraSetNearClipPlane(Scene.camera, 0.05f); - } - } - - TargetCoors.z += min(1.0f, m_fCamBufferedHeight/2.0f); - m_cvecTargetCoorsForFudgeInter = TargetCoors; - - Front = TargetCoors - Source; - m_fRealGroundDist = Front.Magnitude2D(); - m_fMinDistAwayFromCamWhenInterPolating = m_fRealGroundDist; - Front.Normalise(); - GetVectorsReadyForRW(); - TheCamera.m_bCamDirectlyBehind = false; - TheCamera.m_bCamDirectlyInFront = false; - PreviouslyObscured = BuildingCheckObscured; - - ResetStatics = false; -} - -void -CCam::Process_BehindCar(const CVector &CameraTarget, float TargetOrientation, float, float) -{ - FOV = DefaultFOV; - - if(!CamTargetEntity->IsVehicle()) - return; - - CVector TargetCoors = CameraTarget; - TargetCoors.z -= 0.2f; - CA_MAX_DISTANCE = 9.95f; - CA_MIN_DISTANCE = 8.5f; - - CVector Dist = Source - TargetCoors; - float Length = Dist.Magnitude2D(); - m_fDistanceBeforeChanges = Length; - if(Length < 0.002f) - Length = 0.002f; - Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); - if(Length > CA_MAX_DISTANCE){ - Source.x = TargetCoors.x + Dist.x/Length * CA_MAX_DISTANCE; - Source.y = TargetCoors.y + Dist.y/Length * CA_MAX_DISTANCE; - }else if(Length < CA_MIN_DISTANCE){ - Source.x = TargetCoors.x + Dist.x/Length * CA_MIN_DISTANCE; - Source.y = TargetCoors.y + Dist.y/Length * CA_MIN_DISTANCE; - } - TargetCoors.z += 0.8f; - - WorkOutCamHeightWeeCar(TargetCoors, TargetOrientation); - RotCamIfInFrontCar(TargetCoors, TargetOrientation); - FixCamIfObscured(TargetCoors, 1.2f, TargetOrientation); - - Front = TargetCoors - Source; - m_cvecTargetCoorsForFudgeInter = TargetCoors; - ResetStatics = false; - GetVectorsReadyForRW(); -} - -void -CCam::WorkOutCamHeightWeeCar(CVector &TargetCoors, float TargetOrientation) -{ - CColPoint colpoint; - CEntity *ent; - float TargetZOffSet = 0.0f; - static bool PreviouslyFailedRoadHeightCheck = false; - static float RoadHeightFix = 0.0f; - static float RoadHeightFixSpeed = 0.0f; - - if(ResetStatics){ - RoadHeightFix = 0.0f; - RoadHeightFixSpeed = 0.0f; - Alpha = DEGTORAD(25.0f); - AlphaSpeed = 0.0f; - } - float AlphaTarget = DEGTORAD(25.0f); - if(CCullZones::CamNoRain() || CCullZones::PlayerNoRain()) - AlphaTarget = DEGTORAD(14.0f); - WellBufferMe(AlphaTarget, &Alpha, &AlphaSpeed, 0.1f, 0.05f, true); - Source.z = TargetCoors.z + CA_MAX_DISTANCE*Sin(Alpha); - - if(FindPlayerVehicle()){ - m_fUnknownZOffSet = 0.0f; - bool FoundRoad = false; - bool FoundRoof = false; - float RoadZ = 0.0f; - float RoofZ = 0.0f; - - if(CWorld::ProcessVerticalLine(Source, -1000.0f, colpoint, ent, true, false, false, false, false, false, nil) && - ent->IsBuilding()){ - FoundRoad = true; - RoadZ = colpoint.point.z; - } - - if(FoundRoad){ - if(Source.z - RoadZ < 0.9f){ - PreviouslyFailedRoadHeightCheck = true; - TargetZOffSet = RoadZ + 0.9f - Source.z; - }else{ - if(m_bCollisionChecksOn) - PreviouslyFailedRoadHeightCheck = false; - else - TargetZOffSet = 0.0f; - } - }else{ - if(CWorld::ProcessVerticalLine(Source, 1000.0f, colpoint, ent, true, false, false, false, false, false, nil) && - ent->IsBuilding()){ - FoundRoof = true; - RoofZ = colpoint.point.z; - } - if(FoundRoof){ - if(Source.z - RoofZ < 0.9f){ - PreviouslyFailedRoadHeightCheck = true; - TargetZOffSet = RoofZ + 0.9f - Source.z; - }else{ - if(m_bCollisionChecksOn) - PreviouslyFailedRoadHeightCheck = false; - else - TargetZOffSet = 0.0f; - } - } - } - } - - if(TargetZOffSet > RoadHeightFix) - RoadHeightFix = TargetZOffSet; - else - WellBufferMe(TargetZOffSet, &RoadHeightFix, &RoadHeightFixSpeed, 0.27f, 0.1f, false); - - if((colpoint.surfaceB == SURFACE_DEFAULT || colpoint.surfaceB >= SURFACE_METAL6) && - colpoint.surfaceB != SURFACE_STEEL && colpoint.surfaceB != SURFACE_STONE && - RoadHeightFix > 1.4f) - RoadHeightFix = 1.4f; - - Source.z += RoadHeightFix; -} - -void -CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, float TargetHeight) -{ - static float LastTargetAlphaWithCollisionOn = 0.0f; - static float LastTopAlphaSpeed = 0.0f; - static float LastAlphaSpeedStep = 0.0f; - static bool PreviousNearCheckNearClipSmall = false; - - bool CamClear = true; - float ModeAlpha = 0.0f; - - if(ResetStatics){ - LastTargetAlphaWithCollisionOn = 0.0f; - LastTopAlphaSpeed = 0.0f; - LastAlphaSpeedStep = 0.0f; - PreviousNearCheckNearClipSmall = false; - } - - float TopAlphaSpeed = 0.15f; - float AlphaSpeedStep = 0.015f; - - float zoomvalue = TheCamera.CarZoomValueSmooth; - if(zoomvalue < 0.1f) - zoomvalue = 0.1f; - if(TheCamera.CarZoomIndicator == 1.0f) - ModeAlpha = CGeneral::GetATanOfXY(23.0f, zoomvalue); // near - else if(TheCamera.CarZoomIndicator == 2.0f) - ModeAlpha = CGeneral::GetATanOfXY(10.8f, zoomvalue); // mid - else if(TheCamera.CarZoomIndicator == 3.0f) - ModeAlpha = CGeneral::GetATanOfXY(7.0f, zoomvalue); // far - - - float Length = (Source - TargetCoors).Magnitude2D(); - if(m_bCollisionChecksOn){ // there's another variable (on PC) but it's uninitialised - CVector Forward = CamTargetEntity->GetForward(); - float CarAlpha = CGeneral::GetATanOfXY(Forward.Magnitude2D(), Forward.z); - // this shouldn't be necessary.... - while(CarAlpha >= PI) CarAlpha -= 2*PI; - while(CarAlpha < -PI) CarAlpha += 2*PI; - - while(Beta >= PI) Beta -= 2*PI; - while(Beta < -PI) Beta += 2*PI; - - float deltaBeta = Beta - TargetOrientation; - while(deltaBeta >= PI) deltaBeta -= 2*PI; - while(deltaBeta < -PI) deltaBeta += 2*PI; - - float BehindCarNess = Cos(deltaBeta); // 1 if behind car, 0 if side, -1 if in front - CarAlpha = -CarAlpha * BehindCarNess; - if(CarAlpha < -0.01f) - CarAlpha = -0.01f; - - float DeltaAlpha = CarAlpha - Alpha; - while(DeltaAlpha >= PI) DeltaAlpha -= 2*PI; - while(DeltaAlpha < -PI) DeltaAlpha += 2*PI; - // What's this?? wouldn't it make more sense to clamp? - float AngleLimit = DEGTORAD(1.8f); - if(DeltaAlpha < -AngleLimit) - DeltaAlpha += AngleLimit; - else if(DeltaAlpha > AngleLimit) - DeltaAlpha -= AngleLimit; - else - DeltaAlpha = 0.0f; - - // Now the collision - - float TargetAlpha = 0.0f; - bool FoundRoofCenter = false; - bool FoundRoofSide1 = false; - bool FoundRoofSide2 = false; - bool FoundCamRoof = false; - bool FoundCamGround = false; - float CamRoof = 0.0f; - float CarBottom = TargetCoors.z - TargetHeight/2.0f; - - // Check car center - float CarRoof = CWorld::FindRoofZFor3DCoord(TargetCoors.x, TargetCoors.y, CarBottom, &FoundRoofCenter); - - // Check sides of the car - Forward = CamTargetEntity->GetForward(); // we actually still have that... - Forward.Normalise(); // shouldn't be necessary - float CarSideAngle = CGeneral::GetATanOfXY(Forward.x, Forward.y) + PI/2.0f; - float SideX = 2.5f * Cos(CarSideAngle); - float SideY = 2.5f * Sin(CarSideAngle); - CWorld::FindRoofZFor3DCoord(TargetCoors.x + SideX, TargetCoors.y + SideY, CarBottom, &FoundRoofSide1); - CWorld::FindRoofZFor3DCoord(TargetCoors.x - SideX, TargetCoors.y - SideY, CarBottom, &FoundRoofSide2); - - // Now find out at what height we'd like to place the camera - float CamGround = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, TargetCoors.z + Length*Sin(Alpha + ModeAlpha) + m_fCloseInCarHeightOffset, &FoundCamGround); - float CamTargetZ = 0.0f; - if(FoundCamGround){ - // This is the normal case - CamRoof = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, CamGround + TargetHeight, &FoundCamRoof); - CamTargetZ = CamGround + TargetHeight*1.5f + 0.1f; - }else{ - FoundCamRoof = false; - CamTargetZ = TargetCoors.z; - } - - if(FoundRoofCenter && !FoundCamRoof && (FoundRoofSide1 || FoundRoofSide2)){ - // Car is under something but camera isn't - // This seems weird... - TargetAlpha = CGeneral::GetATanOfXY(CA_MAX_DISTANCE, CarRoof - CamTargetZ - 1.5f); - CamClear = false; - } - if(FoundCamRoof){ - // Camera is under something - float roof = FoundRoofCenter ? min(CamRoof, CarRoof) : CamRoof; - // Same weirdness again? - TargetAlpha = CGeneral::GetATanOfXY(CA_MAX_DISTANCE, roof - CamTargetZ - 1.5f); - CamClear = false; - } - while(TargetAlpha >= PI) TargetAlpha -= 2*PI; - while(TargetAlpha < -PI) TargetAlpha += 2*PI; - if(TargetAlpha < DEGTORAD(-7.0f)) - TargetAlpha = DEGTORAD(-7.0f); - - // huh? - if(TargetAlpha > ModeAlpha) - CamClear = true; - // Camera is contrained by collision in some way - PreviousNearCheckNearClipSmall = false; - if(!CamClear){ - PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); - - DeltaAlpha = TargetAlpha - (Alpha + ModeAlpha); - while(DeltaAlpha >= PI) DeltaAlpha -= 2*PI; - while(DeltaAlpha < -PI) DeltaAlpha += 2*PI; - - TopAlphaSpeed = 0.3f; - AlphaSpeedStep = 0.03f; - } - - // Now do things if CamClear...but what is that anyway? - float CamZ = TargetCoors.z + Length*Sin(Alpha + DeltaAlpha + ModeAlpha) + m_fCloseInCarHeightOffset; - bool FoundGround, FoundRoof; - float CamGround2 = CWorld::FindGroundZFor3DCoord(Source.x, Source.y, CamZ, &FoundGround); - if(FoundGround){ - if(CamClear) - if(CamZ - CamGround2 < 1.5f){ - PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); - - float a; - if(Length == 0.0f || CamGround2 + 1.5f - TargetCoors.z == 0.0f) - a = Alpha; - else - a = CGeneral::GetATanOfXY(Length, CamGround2 + 1.5f - TargetCoors.z); - while(a > PI) a -= 2*PI; - while(a < -PI) a += 2*PI; - DeltaAlpha = a - Alpha; - } - }else{ - if(CamClear){ - float CamRoof2 = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, CamZ, &FoundRoof); - if(FoundRoof && CamZ - CamRoof2 < 1.5f){ - PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); - - if(CamRoof2 > TargetCoors.z + 3.5f) - CamRoof2 = TargetCoors.z + 3.5f; - - float a; - if(Length == 0.0f || CamRoof2 + 1.5f - TargetCoors.z == 0.0f) - a = Alpha; - else - a = CGeneral::GetATanOfXY(Length, CamRoof2 + 1.5f - TargetCoors.z); - while(a > PI) a -= 2*PI; - while(a < -PI) a += 2*PI; - DeltaAlpha = a - Alpha; - } - } - } - - LastTargetAlphaWithCollisionOn = DeltaAlpha + Alpha; - LastTopAlphaSpeed = TopAlphaSpeed; - LastAlphaSpeedStep = AlphaSpeedStep; - }else{ - if(PreviousNearCheckNearClipSmall) - RwCameraSetNearClipPlane(Scene.camera, 0.9f); - } - - WellBufferMe(LastTargetAlphaWithCollisionOn, &Alpha, &AlphaSpeed, LastTopAlphaSpeed, LastAlphaSpeedStep, true); - - Source.z = TargetCoors.z + Sin(Alpha + ModeAlpha)*Length + m_fCloseInCarHeightOffset; -} - -// Rotate cam behind the car when the car is moving forward -bool -CCam::RotCamIfInFrontCar(CVector &TargetCoors, float TargetOrientation) -{ - bool MovingForward = false; - CPhysical *phys = (CPhysical*)CamTargetEntity; - - float ForwardSpeed = DotProduct(phys->GetForward(), phys->GetSpeed(CVector(0.0f, 0.0f, 0.0f))); - if(ForwardSpeed > 0.02f) - MovingForward = true; - - float Dist = (Source - TargetCoors).Magnitude2D(); - - float DeltaBeta = TargetOrientation - Beta; - while(DeltaBeta >= PI) DeltaBeta -= 2*PI; - while(DeltaBeta < -PI) DeltaBeta += 2*PI; - - if(Abs(DeltaBeta) > DEGTORAD(20.0f) && MovingForward && TheCamera.m_uiTransitionState == 0) - m_bFixingBeta = true; - - CPad *pad = CPad::GetPad(0); - if(!(pad->GetLookBehindForCar() || pad->GetLookBehindForPed() || pad->GetLookLeft() || pad->GetLookRight())) - if(DirectionWasLooking != LOOKING_FORWARD) - TheCamera.m_bCamDirectlyBehind = true; - - if(!m_bFixingBeta && !TheCamera.m_bUseTransitionBeta && !TheCamera.m_bCamDirectlyBehind && !TheCamera.m_bCamDirectlyInFront) - return false; - - bool SetBeta = false; - if(TheCamera.m_bCamDirectlyBehind || TheCamera.m_bCamDirectlyInFront || TheCamera.m_bUseTransitionBeta) - if(&TheCamera.Cams[TheCamera.ActiveCam] == this) - SetBeta = true; - - if(m_bFixingBeta || SetBeta){ - WellBufferMe(TargetOrientation, &Beta, &BetaSpeed, 0.15f, 0.007f, true); - - if(TheCamera.m_bCamDirectlyBehind && &TheCamera.Cams[TheCamera.ActiveCam] == this) - Beta = TargetOrientation; - if(TheCamera.m_bCamDirectlyInFront && &TheCamera.Cams[TheCamera.ActiveCam] == this) - Beta = TargetOrientation + PI; - if(TheCamera.m_bUseTransitionBeta && &TheCamera.Cams[TheCamera.ActiveCam] == this) - Beta = m_fTransitionBeta; - - Source.x = TargetCoors.x - Cos(Beta)*Dist; - Source.y = TargetCoors.y - Sin(Beta)*Dist; - - // Check if we're done - DeltaBeta = TargetOrientation - Beta; - while(DeltaBeta >= PI) DeltaBeta -= 2*PI; - while(DeltaBeta < -PI) DeltaBeta += 2*PI; - if(Abs(DeltaBeta) < DEGTORAD(2.0f)) - m_bFixingBeta = false; - } - TheCamera.m_bCamDirectlyBehind = false; - TheCamera.m_bCamDirectlyInFront = false; - return true; -} - -// Move the cam to avoid clipping through buildings -bool -CCam::FixCamIfObscured(CVector &TargetCoors, float TargetHeight, float TargetOrientation) -{ - CVector Target = TargetCoors; - bool UseEntityPos = false; - CVector EntityPos; - static CColPoint colPoint; - static bool LastObscured = false; - - if(Mode == MODE_BEHINDCAR) - Target.z += TargetHeight/2.0f; - if(Mode == MODE_CAM_ON_A_STRING){ - UseEntityPos = true; - Target.z += TargetHeight/2.0f; - EntityPos = CamTargetEntity->GetPosition(); - } - - CVector TempSource = Source; - - bool Obscured1 = false; - bool Obscured2 = false; - bool Fix1 = false; - float Dist1 = 0.0f; - float Dist2 = 0.0f; - CEntity *ent; - if(m_bCollisionChecksOn || LastObscured){ - Obscured1 = CWorld::ProcessLineOfSight(Target, TempSource, colPoint, ent, true, false, false, true, false, true, true); - if(Obscured1){ - Dist1 = (Target - colPoint.point).Magnitude2D(); - Fix1 = true; - if(UseEntityPos) - Obscured1 = CWorld::ProcessLineOfSight(EntityPos, TempSource, colPoint, ent, true, false, false, true, false, true, true); - }else if(m_bFixingBeta){ - float d = (TempSource - Target).Magnitude(); - TempSource.x = Target.x - d*Cos(TargetOrientation); - TempSource.y = Target.y - d*Sin(TargetOrientation); - - // same check again - Obscured2 = CWorld::ProcessLineOfSight(Target, TempSource, colPoint, ent, true, false, false, true, false, true, true); - if(Obscured2){ - Dist2 = (Target - colPoint.point).Magnitude2D(); - if(UseEntityPos) - Obscured2 = CWorld::ProcessLineOfSight(EntityPos, TempSource, colPoint, ent, true, false, false, true, false, true, true); - } - } - LastObscured = Obscured1 || Obscured2; - } - - // nothing to do - if(!LastObscured) - return false; - - if(Fix1){ - Source.x = Target.x - Cos(Beta)*Dist1; - Source.y = Target.y - Sin(Beta)*Dist1; - if(Mode == MODE_BEHINDCAR) - Source = colPoint.point; - }else{ - WellBufferMe(Dist2, &m_fDistanceBeforeChanges, &DistanceSpeed, 0.2f, 0.025f, false); - Source.x = Target.x - Cos(Beta)*m_fDistanceBeforeChanges; - Source.y = Target.y - Sin(Beta)*m_fDistanceBeforeChanges; - } - - if(ResetStatics){ - m_fDistanceBeforeChanges = (Source - Target).Magnitude2D(); - DistanceSpeed = 0.0f; - Source.x = colPoint.point.x; - Source.y = colPoint.point.y; - } - return true; -} - -void -CCam::Process_Cam_On_A_String(const CVector &CameraTarget, float TargetOrientation, float, float) -{ - if(!CamTargetEntity->IsVehicle()) - return; - - FOV = DefaultFOV; - - if(ResetStatics){ - AlphaSpeed = 0.0f; - if(TheCamera.m_bIdleOn) - TheCamera.m_uiTimeWeEnteredIdle = CTimer::GetTimeInMilliseconds(); - } - - CBaseModelInfo *mi = CModelInfo::GetModelInfo(CamTargetEntity->GetModelIndex()); - CVector Dimensions = mi->GetColModel()->boundingBox.max - mi->GetColModel()->boundingBox.min; - float BaseDist = Dimensions.Magnitude2D(); - - CVector TargetCoors = CameraTarget; - TargetCoors.z += Dimensions.z - 0.1f; // final - Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); - while(Alpha >= PI) Alpha -= 2*PI; - while(Alpha < -PI) Alpha += 2*PI; - while(Beta >= PI) Beta -= 2*PI; - while(Beta < -PI) Beta += 2*PI; - - m_fDistanceBeforeChanges = (Source - TargetCoors).Magnitude2D(); - - Cam_On_A_String_Unobscured(TargetCoors, BaseDist); - WorkOutCamHeight(TargetCoors, TargetOrientation, Dimensions.z); - RotCamIfInFrontCar(TargetCoors, TargetOrientation); - FixCamIfObscured(TargetCoors, Dimensions.z, TargetOrientation); - FixCamWhenObscuredByVehicle(TargetCoors); - - m_cvecTargetCoorsForFudgeInter = TargetCoors; - Front = TargetCoors - Source; - Front.Normalise(); - GetVectorsReadyForRW(); - ResetStatics = false; -} - -// Basic Cam on a string algorithm -void -CCam::Cam_On_A_String_Unobscured(const CVector &TargetCoors, float BaseDist) -{ - CA_MAX_DISTANCE = BaseDist + 0.1f + TheCamera.CarZoomValueSmooth; - CA_MIN_DISTANCE = min(BaseDist*0.6f, 3.5f); - - CVector Dist = Source - TargetCoors; - - if(ResetStatics) - Source = TargetCoors + Dist*(CA_MAX_DISTANCE + 1.0f); - - float Length = Dist.Magnitude2D(); - if(Length < 0.001f){ - // This probably shouldn't happen. reset view - CVector Forward = CamTargetEntity->GetForward(); - Forward.z = 0.0f; - Forward.Normalise(); - Source = TargetCoors - Forward*CA_MAX_DISTANCE; - Dist = Source - TargetCoors; - Length = Dist.Magnitude2D(); - } - - if(Length > CA_MAX_DISTANCE){ - Source.x = TargetCoors.x + Dist.x/Length * CA_MAX_DISTANCE; - Source.y = TargetCoors.y + Dist.y/Length * CA_MAX_DISTANCE; - }else if(Length < CA_MIN_DISTANCE){ - Source.x = TargetCoors.x + Dist.x/Length * CA_MIN_DISTANCE; - Source.y = TargetCoors.y + Dist.y/Length * CA_MIN_DISTANCE; - } -} - -void -CCam::FixCamWhenObscuredByVehicle(const CVector &TargetCoors) -{ - // BUG? is this never reset - static float HeightFixerCarsObscuring = 0.0f; - static float HeightFixerCarsObscuringSpeed = 0.0f; - CColPoint colPoint; - CEntity *entity; - - float HeightTarget = 0.0f; - if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, false, true, false, false, false, false, false)){ - CBaseModelInfo *mi = CModelInfo::GetModelInfo(entity->GetModelIndex()); - HeightTarget = mi->GetColModel()->boundingBox.max.z + 1.0f + TargetCoors.z - Source.z; - if(HeightTarget < 0.0f) - HeightTarget = 0.0f; - } - WellBufferMe(HeightTarget, &HeightFixerCarsObscuring, &HeightFixerCarsObscuringSpeed, 0.2f, 0.025f, false); - Source.z += HeightFixerCarsObscuring; -} - -bool -CCam::Using3rdPersonMouseCam() -{ - return CCamera::m_bUseMouse3rdPerson && - (Mode == MODE_FOLLOWPED || - TheCamera.m_bPlayerIsInGarage && - FindPlayerPed() && FindPlayerPed()->m_nPedState != PED_DRIVING && - Mode != MODE_TOPDOWN && this->CamTargetEntity == FindPlayerPed()); -} - -bool -CCam::GetWeaponFirstPersonOn() -{ - CEntity *target = this->CamTargetEntity; - if (target && target->IsPed()) - return ((CPed*)target)->GetWeapon()->m_bAddRotOffset; - - return false; -} - float CCamera::Find3rdPersonQuickAimPitch(void) { @@ -1478,22 +343,4 @@ STARTPATCHES InjectHook(0x46B560, &CCamera::FinishCutscene, PATCH_JUMP); InjectHook(0x46FF30, &CCamera::SetZoomValueFollowPedScript, PATCH_JUMP); InjectHook(0x46FF90, &CCamera::SetZoomValueCamStringScript, PATCH_JUMP); - - - InjectHook(0x456F40, WellBufferMe, PATCH_JUMP); - InjectHook(0x4582F0, &CCam::GetVectorsReadyForRW, PATCH_JUMP); - InjectHook(0x457710, &CCam::DoAverageOnVector, PATCH_JUMP); - InjectHook(0x458060, &CCam::GetPedBetaAngleForClearView, PATCH_JUMP); - InjectHook(0x457210, &CCam::Cam_On_A_String_Unobscured, PATCH_JUMP); - InjectHook(0x457A80, &CCam::FixCamWhenObscuredByVehicle, PATCH_JUMP); - InjectHook(0x457B90, &CCam::FixCamIfObscured, PATCH_JUMP); - InjectHook(0x465DA0, &CCam::RotCamIfInFrontCar, PATCH_JUMP); - InjectHook(0x4662D0, &CCam::WorkOutCamHeightWeeCar, PATCH_JUMP); - InjectHook(0x466650, &CCam::WorkOutCamHeight, PATCH_JUMP); - - InjectHook(0x45E3A0, &CCam::Process_FollowPed, PATCH_JUMP); - InjectHook(0x45BE60, &CCam::Process_BehindCar, PATCH_JUMP); - InjectHook(0x45C090, &CCam::Process_Cam_On_A_String, PATCH_JUMP); - - InjectHook(0x473250, &CCamera::dtor, PATCH_JUMP); ENDPATCHES diff --git a/src/core/Camera.h b/src/core/Camera.h index 980af5c1..48f2d27a 100644 --- a/src/core/Camera.h +++ b/src/core/Camera.h @@ -5,6 +5,8 @@ class CEntity; class CPed; class CAutomobile; +extern int16 &DebugCamMode; + #define NUMBER_OF_VECTORS_FOR_AVERAGE 2 struct CCam @@ -66,17 +68,17 @@ struct CCam bool m_bTheHeightFixerVehicleIsATrain; bool LookBehindCamWasInFront; bool LookingBehind; - bool LookingLeft; // 32 + bool LookingLeft; bool LookingRight; bool ResetStatics; //for interpolation type stuff to work bool Rotating; int16 Mode; // CameraMode - uint32 m_uiFinishTime; // 52 + uint32 m_uiFinishTime; int m_iDoCollisionChecksOnFrameNum; int m_iDoCollisionCheckEveryNumOfFrames; - int m_iFrameNumWereAt; // 64 + int m_iFrameNumWereAt; int m_iRunningVectorArrayPos; int m_iRunningVectorCounter; int DirectionWasLooking; @@ -85,9 +87,9 @@ struct CCam float f_Roll; //used for adding a slight roll to the camera in the float f_rollSpeed; float m_fSyphonModeTargetZOffSet; - float m_fUnknownZOffSet; + float m_fRoadOffSet; float m_fAmountFractionObscured; - float m_fAlphaSpeedOverOneFrame; // 100 + float m_fAlphaSpeedOverOneFrame; float m_fBetaSpeedOverOneFrame; float m_fBufferedTargetBeta; float m_fBufferedTargetOrientation; @@ -95,7 +97,7 @@ struct CCam float m_fCamBufferedHeight; float m_fCamBufferedHeightSpeed; float m_fCloseInPedHeightOffset; - float m_fCloseInPedHeightOffsetSpeed; // 132 + float m_fCloseInPedHeightOffsetSpeed; float m_fCloseInCarHeightOffset; float m_fCloseInCarHeightOffsetSpeed; float m_fDimensionOfHighestNearCar; @@ -103,7 +105,7 @@ struct CCam float m_fFovSpeedOverOneFrame; float m_fMinDistAwayFromCamWhenInterPolating; float m_fPedBetweenCameraHeightOffset; - float m_fPlayerInFrontSyphonAngleOffSet; // 164 + float m_fPlayerInFrontSyphonAngleOffSet; float m_fRadiusForDead; float m_fRealGroundDist; //used for follow ped mode float m_fTargetBeta; @@ -111,7 +113,7 @@ struct CCam float m_fTransitionBeta; float m_fTrueBeta; - float m_fTrueAlpha; // 200 + float m_fTrueAlpha; float m_fInitialPlayerOrientation; //used for first person float Alpha; @@ -120,34 +122,25 @@ struct CCam float FOVSpeed; float Beta; float BetaSpeed; - float Distance; // 232 + float Distance; float DistanceSpeed; float CA_MIN_DISTANCE; float CA_MAX_DISTANCE; float SpeedVar; - // ped onfoot zoom distance - float m_fTargetZoomGroundOne; - float m_fTargetZoomGroundTwo; // 256 - float m_fTargetZoomGroundThree; - // ped onfoot alpha angle offset - float m_fTargetZoomOneZExtra; - float m_fTargetZoomTwoZExtra; - float m_fTargetZoomThreeZExtra; - - float m_fTargetZoomZCloseIn; - float m_fMinRealGroundDist; - float m_fTargetCloseInDist; + CVector m_cvecSourceSpeedOverOneFrame; + CVector m_cvecTargetSpeedOverOneFrame; + CVector m_cvecUpOverOneFrame; - CVector m_cvecTargetCoorsForFudgeInter; // 360 - CVector m_cvecCamFixedModeVector; // 372 - CVector m_cvecCamFixedModeSource; // 384 - CVector m_cvecCamFixedModeUpOffSet; // 396 - CVector m_vecLastAboveWaterCamPosition; //408 //helper for when the player has gone under the water - CVector m_vecBufferedPlayerBodyOffset; // 420 + CVector m_cvecTargetCoorsForFudgeInter; + CVector m_cvecCamFixedModeVector; + CVector m_cvecCamFixedModeSource; + CVector m_cvecCamFixedModeUpOffSet; + CVector m_vecLastAboveWaterCamPosition; //helper for when the player has gone under the water + CVector m_vecBufferedPlayerBodyOffset; // The three vectors that determine this camera for this frame - CVector Front; // 432 // Direction of looking in + CVector Front; // Direction of looking in CVector Source; // Coors in world space CVector SourceBeforeLookBehind; CVector Up; // Just that @@ -162,6 +155,10 @@ struct CCam bool m_bFirstPersonRunAboutActive; + CCam(void) { Init(); } + void Init(void); + void Process(void); + void ProcessSpecialHeightRoutines(void); void GetVectorsReadyForRW(void); CVector DoAverageOnVector(const CVector &vec); float GetPedBetaAngleForClearView(const CVector &Target, float Dist, float BetaOffset, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies); @@ -171,13 +168,44 @@ struct CCam bool FixCamIfObscured(CVector &TargetCoors, float TargetHeight, float TargetOrientation); void Cam_On_A_String_Unobscured(const CVector &TargetCoors, float BaseDist); void FixCamWhenObscuredByVehicle(const CVector &TargetCoors); - bool Using3rdPersonMouseCam(); - bool GetWeaponFirstPersonOn(); + void LookBehind(void); + void LookLeft(void); + void LookRight(void); + void ClipIfPedInFrontOfPlayer(void); + void KeepTrackOfTheSpeed(const CVector &source, const CVector &target, const CVector &up, const float &alpha, const float &beta, const float &fov); + bool Using3rdPersonMouseCam(void); + bool GetWeaponFirstPersonOn(void); + bool IsTargetInWater(const CVector &CamCoors); + void AvoidWallsTopDownPed(const CVector &TargetCoors, const CVector &Offset, float *Adjuster, float *AdjusterSpeed, float yDistLimit); + void PrintMode(void); - void Process_Debug(float *vec, float a, float b, float c); + void Process_Debug(const CVector&, float, float, float); + void Process_Editor(const CVector&, float, float, float); + void Process_ModelView(const CVector &CameraTarget, float, float, float); void Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrientation, float, float); void Process_BehindCar(const CVector &CameraTarget, float TargetOrientation, float, float); void Process_Cam_On_A_String(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_TopDown(const CVector &CameraTarget, float TargetOrientation, float SpeedVar, float TargetSpeedVar); + void Process_TopDownPed(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_Rocket(const CVector &CameraTarget, float, float, float); + void Process_M16_1stPerson(const CVector &CameraTarget, float, float, float); + void Process_1stPerson(const CVector &CameraTarget, float, float, float); + void Process_1rstPersonPedOnPC(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_Sniper(const CVector &CameraTarget, float, float, float); + void Process_Syphon(const CVector &CameraTarget, float, float, float); + void Process_Syphon_Crim_In_Front(const CVector &CameraTarget, float, float, float); + void Process_BehindBoat(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_Fight_Cam(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_FlyBy(const CVector&, float, float, float); + void Process_WheelCam(const CVector&, float, float, float); + void Process_Fixed(const CVector &CameraTarget, float, float, float); + void Process_Player_Fallen_Water(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_Circle(const CVector &CameraTarget, float, float, float); + void Process_SpecialFixedForSyphon(const CVector &CameraTarget, float, float, float); + void ProcessPedsDeadBaby(void); + bool ProcessArrestCamOne(void); + bool ProcessArrestCamTwo(void); }; static_assert(sizeof(CCam) == 0x1A4, "CCam: wrong size"); static_assert(offsetof(CCam, Alpha) == 0xA8, "CCam: error"); @@ -223,6 +251,7 @@ enum FADE_OUT = 0, FADE_IN, + FADE_NONE }; enum @@ -445,8 +474,8 @@ uint32 unknown; uint32 m_fScriptTimeForInterPolation; -int16 m_iFadingDirection; -int m_iModeObbeCamIsInForCar; + int16 m_iFadingDirection; + int m_iModeObbeCamIsInForCar; int16 m_iModeToGoTo; int16 m_iMusicFadingDirection; int16 m_iTypeOfSwitch; @@ -493,6 +522,7 @@ int m_iModeObbeCamIsInForCar; void TakeControlNoEntity(const CVector&, int16, int32); void SetCamPositionForFixedMode(const CVector&, const CVector&); bool GetFading(); + int GetFadingDirection(); void Init(); void SetRwCamera(RwCamera*); @@ -525,8 +555,12 @@ static_assert(offsetof(CCamera, m_uiTransitionState) == 0x89, "CCamera: error"); static_assert(offsetof(CCamera, m_uiTimeTransitionStart) == 0x94, "CCamera: error"); static_assert(offsetof(CCamera, m_BlurBlue) == 0x9C, "CCamera: error"); static_assert(offsetof(CCamera, Cams) == 0x1A4, "CCamera: error"); +static_assert(offsetof(CCamera, pToGarageWeAreIn) == 0x690, "CCamera: error"); +static_assert(offsetof(CCamera, m_PreviousCameraPosition) == 0x6B0, "CCamera: error"); static_assert(offsetof(CCamera, m_vecCutSceneOffset) == 0x6F8, "CCamera: error"); +static_assert(offsetof(CCamera, m_arrPathArray) == 0x7a8, "CCamera: error"); static_assert(sizeof(CCamera) == 0xE9D8, "CCamera: wrong size"); + extern CCamera &TheCamera; -void CamShakeNoPos(CCamera*, float); \ No newline at end of file +void CamShakeNoPos(CCamera*, float); diff --git a/src/core/Debug.cpp b/src/core/Debug.cpp index bdcbaf04..2b713198 100644 --- a/src/core/Debug.cpp +++ b/src/core/Debug.cpp @@ -89,3 +89,49 @@ CDebug::DebugDisplayTextBuffer() } #endif } + + +// custom + +CDebug::ScreenStr CDebug::ms_aScreenStrs[MAX_SCREEN_STRS]; +int CDebug::ms_nScreenStrs; + +void +CDebug::DisplayScreenStrings() +{ + int i; + + + CFont::SetPropOn(); + CFont::SetBackgroundOff(); + CFont::SetScale(1.0f, 1.0f); + CFont::SetCentreOff(); + CFont::SetRightJustifyOff(); + CFont::SetJustifyOff(); + CFont::SetRightJustifyWrap(0.0f); + CFont::SetWrapx(9999.0f); + CFont::SetBackGroundOnlyTextOff(); + CFont::SetFontStyle(FONT_BANK); + + for(i = 0; i < ms_nScreenStrs; i++){ + AsciiToUnicode(ms_aScreenStrs[i].str, gUString); + CFont::SetColor(CRGBA(0, 0, 0, 255)); + CFont::PrintString(ms_aScreenStrs[i].x, ms_aScreenStrs[i].y, gUString); + CFont::SetColor(CRGBA(255, 255, 255, 255)); + CFont::PrintString(ms_aScreenStrs[i].x+1, ms_aScreenStrs[i].y+1, gUString); + } + CFont::DrawFonts(); + + ms_nScreenStrs = 0; +} + +void +CDebug::PrintAt(const char *str, int x, int y) +{ + if(ms_nScreenStrs >= MAX_SCREEN_STRS) + return; + strncpy(ms_aScreenStrs[ms_nScreenStrs].str, str, 256); + ms_aScreenStrs[ms_nScreenStrs].x = x*12; + ms_aScreenStrs[ms_nScreenStrs].y = y*22; + ms_nScreenStrs++; +} diff --git a/src/core/Debug.h b/src/core/Debug.h index 444a0cf5..d169a0b4 100644 --- a/src/core/Debug.h +++ b/src/core/Debug.h @@ -6,15 +6,29 @@ class CDebug { MAX_LINES = 15, MAX_STR_LEN = 80, + + MAX_SCREEN_STRS = 100, }; static int16 ms_nCurrentTextLine; static char ms_aTextBuffer[MAX_LINES][MAX_STR_LEN]; + // custom + struct ScreenStr { + int x, y; + char str[256]; + }; + static ScreenStr ms_aScreenStrs[MAX_SCREEN_STRS]; + static int ms_nScreenStrs; + public: static void DebugInitTextBuffer(); static void DebugDisplayTextBuffer(); static void DebugAddText(const char *str); + + // custom + static void PrintAt(const char *str, int x, int y); + static void DisplayScreenStrings(); }; extern bool gbDebugStuffInRelease; diff --git a/src/core/Pad.h b/src/core/Pad.h index 09691128..fec21df3 100644 --- a/src/core/Pad.h +++ b/src/core/Pad.h @@ -399,6 +399,8 @@ public: bool GetLeftShoulder2JustDown() { return !!(NewState.LeftShoulder2 && !OldState.LeftShoulder2); } bool GetRightShoulder1JustDown() { return !!(NewState.RightShoulder1 && !OldState.RightShoulder1); } bool GetRightShoulder2JustDown() { return !!(NewState.RightShoulder2 && !OldState.RightShoulder2); } + bool GetLeftShockJustDown() { return !!(NewState.LeftShock && !OldState.LeftShock); } + bool GetRightShockJustDown() { return !!(NewState.RightShock && !OldState.RightShock); } bool GetStartJustDown() { return !!(NewState.Start && !OldState.Start); } bool GetLeftStickXJustDown() { return !!(NewState.LeftStickX && !OldState.LeftStickX); } bool GetLeftStickYJustDown() { return !!(NewState.LeftStickY && !OldState.LeftStickY); } @@ -422,6 +424,10 @@ public: bool GetLeftShoulder2(void) { return !!NewState.LeftShoulder2; } bool GetRightShoulder1(void) { return !!NewState.RightShoulder1; } bool GetRightShoulder2(void) { return !!NewState.RightShoulder2; } + int16 GetLeftStickX(void) { return NewState.LeftStickX; } + int16 GetLeftStickY(void) { return NewState.LeftStickY; } + int16 GetRightStickX(void) { return NewState.RightStickX; } + int16 GetRightStickY(void) { return NewState.RightStickY; } bool ArePlayerControlsDisabled(void) { return DisablePlayerControls != PLAYERCONTROL_ENABLED; } }; diff --git a/src/core/World.cpp b/src/core/World.cpp index cbceb292..1dda1056 100644 --- a/src/core/World.cpp +++ b/src/core/World.cpp @@ -20,11 +20,12 @@ #include "Replay.h" #include "Population.h" +CColPoint *gaTempSphereColPoints = (CColPoint*)0x6E64C0; // [32] + CPtrList *CWorld::ms_bigBuildingsList = (CPtrList*)0x6FAB60; CPtrList &CWorld::ms_listMovingEntityPtrs = *(CPtrList*)0x8F433C; CSector (*CWorld::ms_aSectors)[NUMSECTORS_X] = (CSector (*)[NUMSECTORS_Y])0x665608; uint16 &CWorld::ms_nCurrentScanCode = *(uint16*)0x95CC64; -CColPoint &CWorld::ms_testSpherePoint = *(CColPoint*)0x6E64C0; uint8 &CWorld::PlayerInFocus = *(uint8 *)0x95CD61; CPlayerInfo (&CWorld::Players)[NUMPLAYERS] = *(CPlayerInfo (*)[NUMPLAYERS])*(uintptr*)0x9412F0; @@ -609,9 +610,9 @@ CWorld::GetIsLineOfSightSectorListClear(CPtrList &list, const CColLine &line, bo } void -CWorld::FindObjectsInRangeSectorList(CPtrList &list, CVector ¢re, float distance, bool ignoreZ, short *nextObject, short lastObject, CEntity **objects) +CWorld::FindObjectsInRangeSectorList(CPtrList &list, CVector ¢re, float radius, bool ignoreZ, short *nextObject, short lastObject, CEntity **objects) { - float distSqr = distance * distance; + float radiusSqr = radius * radius; float objDistSqr; for (CPtrNode *node = list.first; node; node = node->next) { @@ -625,7 +626,7 @@ CWorld::FindObjectsInRangeSectorList(CPtrList &list, CVector ¢re, float dist else objDistSqr = diff.MagnitudeSqr(); - if (objDistSqr < distSqr && *nextObject < lastObject) { + if (objDistSqr < radiusSqr && *nextObject < lastObject) { if (objects) { objects[*nextObject] = object; } @@ -636,22 +637,22 @@ CWorld::FindObjectsInRangeSectorList(CPtrList &list, CVector ¢re, float dist } void -CWorld::FindObjectsInRange(CVector ¢re, float distance, bool ignoreZ, short *nextObject, short lastObject, CEntity **objects, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies) +CWorld::FindObjectsInRange(CVector ¢re, float radius, bool ignoreZ, short *nextObject, short lastObject, CEntity **objects, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies) { - int minX = GetSectorIndexX(centre.x - distance); + int minX = GetSectorIndexX(centre.x - radius); if (minX <= 0) minX = 0; - int minY = GetSectorIndexY(centre.y - distance); + int minY = GetSectorIndexY(centre.y - radius); if (minY <= 0) minY = 0; - int maxX = GetSectorIndexX(centre.x + distance); + int maxX = GetSectorIndexX(centre.x + radius); #ifdef FIX_BUGS if (maxX >= NUMSECTORS_X) maxX = NUMSECTORS_X - 1; #else if (maxX >= NUMSECTORS_X) maxX = NUMSECTORS_X; #endif - int maxY = GetSectorIndexY(centre.y + distance); + int maxY = GetSectorIndexY(centre.y + radius); #ifdef FIX_BUGS if (maxY >= NUMSECTORS_Y) maxY = NUMSECTORS_Y - 1; #else @@ -665,48 +666,48 @@ CWorld::FindObjectsInRange(CVector ¢re, float distance, bool ignoreZ, short for(int curX = minX; curX <= maxX; curX++) { CSector *sector = GetSector(curX, curY); if (checkBuildings) { - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_BUILDINGS], centre, distance, ignoreZ, nextObject, lastObject, objects); - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_BUILDINGS_OVERLAP], centre, distance, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_BUILDINGS], centre, radius, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_BUILDINGS_OVERLAP], centre, radius, ignoreZ, nextObject, lastObject, objects); } if (checkVehicles) { - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_VEHICLES], centre, distance, ignoreZ, nextObject, lastObject, objects); - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_VEHICLES_OVERLAP], centre, distance, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_VEHICLES], centre, radius, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_VEHICLES_OVERLAP], centre, radius, ignoreZ, nextObject, lastObject, objects); } if (checkPeds) { - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_PEDS], centre, distance, ignoreZ, nextObject, lastObject, objects); - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_PEDS_OVERLAP], centre, distance, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_PEDS], centre, radius, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_PEDS_OVERLAP], centre, radius, ignoreZ, nextObject, lastObject, objects); } if (checkObjects) { - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_OBJECTS], centre, distance, ignoreZ, nextObject, lastObject, objects); - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_OBJECTS_OVERLAP], centre, distance, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_OBJECTS], centre, radius, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_OBJECTS_OVERLAP], centre, radius, ignoreZ, nextObject, lastObject, objects); } if (checkDummies) { - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_DUMMIES], centre, distance, ignoreZ, nextObject, lastObject, objects); - FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_DUMMIES_OVERLAP], centre, distance, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_DUMMIES], centre, radius, ignoreZ, nextObject, lastObject, objects); + FindObjectsInRangeSectorList(sector->m_lists[ENTITYLIST_DUMMIES_OVERLAP], centre, radius, ignoreZ, nextObject, lastObject, objects); } } } } CEntity* -CWorld::TestSphereAgainstWorld(CVector centre, float distance, CEntity *entityToIgnore, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies, bool ignoreSomeObjects) +CWorld::TestSphereAgainstWorld(CVector centre, float radius, CEntity *entityToIgnore, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies, bool ignoreSomeObjects) { CEntity* foundE = nil; - int minX = GetSectorIndexX(centre.x - distance); + int minX = GetSectorIndexX(centre.x - radius); if (minX <= 0) minX = 0; - int minY = GetSectorIndexY(centre.y - distance); + int minY = GetSectorIndexY(centre.y - radius); if (minY <= 0) minY = 0; - int maxX = GetSectorIndexX(centre.x + distance); + int maxX = GetSectorIndexX(centre.x + radius); #ifdef FIX_BUGS if (maxX >= NUMSECTORS_X) maxX = NUMSECTORS_X - 1; #else if (maxX >= NUMSECTORS_X) maxX = NUMSECTORS_X; #endif - int maxY = GetSectorIndexY(centre.y + distance); + int maxY = GetSectorIndexY(centre.y + radius); #ifdef FIX_BUGS if (maxY >= NUMSECTORS_Y) maxY = NUMSECTORS_Y - 1; #else @@ -719,47 +720,47 @@ CWorld::TestSphereAgainstWorld(CVector centre, float distance, CEntity *entityTo for (int curX = minX; curX <= maxX; curX++) { CSector* sector = GetSector(curX, curY); if (checkBuildings) { - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_BUILDINGS], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_BUILDINGS], centre, radius, entityToIgnore, false); if (foundE) return foundE; - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_BUILDINGS_OVERLAP], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_BUILDINGS_OVERLAP], centre, radius, entityToIgnore, false); if (foundE) return foundE; } if (checkVehicles) { - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_VEHICLES], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_VEHICLES], centre, radius, entityToIgnore, false); if (foundE) return foundE; - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_VEHICLES_OVERLAP], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_VEHICLES_OVERLAP], centre, radius, entityToIgnore, false); if (foundE) return foundE; } if (checkPeds) { - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_PEDS], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_PEDS], centre, radius, entityToIgnore, false); if (foundE) return foundE; - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_PEDS_OVERLAP], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_PEDS_OVERLAP], centre, radius, entityToIgnore, false); if (foundE) return foundE; } if (checkObjects) { - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_OBJECTS], centre, distance, entityToIgnore, ignoreSomeObjects); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_OBJECTS], centre, radius, entityToIgnore, ignoreSomeObjects); if (foundE) return foundE; - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_OBJECTS_OVERLAP], centre, distance, entityToIgnore, ignoreSomeObjects); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_OBJECTS_OVERLAP], centre, radius, entityToIgnore, ignoreSomeObjects); if (foundE) return foundE; } if (checkDummies) { - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_DUMMIES], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_DUMMIES], centre, radius, entityToIgnore, false); if (foundE) return foundE; - foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_DUMMIES_OVERLAP], centre, distance, entityToIgnore, false); + foundE = TestSphereAgainstSectorList(sector->m_lists[ENTITYLIST_DUMMIES_OVERLAP], centre, radius, entityToIgnore, false); if (foundE) return foundE; } @@ -806,7 +807,7 @@ CWorld::TestSphereAgainstSectorList(CPtrList &list, CVector spherePos, float rad if (e->GetBoundRadius() + radius > distance) { CColModel *eCol = CModelInfo::GetModelInfo(e->m_modelIndex)->GetColModel(); int collidedSpheres = CCollision::ProcessColModels(sphereMat, sphereCol, e->GetMatrix(), - *eCol, &ms_testSpherePoint, nil, nil); + *eCol, gaTempSphereColPoints, nil, nil); if (collidedSpheres != 0 || (e->IsVehicle() && ((CVehicle*)e)->m_vehType == VEHICLE_TYPE_CAR && diff --git a/src/core/World.h b/src/core/World.h index 1ad65ac4..4b19e629 100644 --- a/src/core/World.h +++ b/src/core/World.h @@ -60,8 +60,6 @@ class CWorld static uint16 &ms_nCurrentScanCode; public: - static CColPoint& ms_testSpherePoint; - static uint8 &PlayerInFocus; static CPlayerInfo (&Players)[NUMPLAYERS]; static CEntity *&pIgnoreEntity; @@ -101,7 +99,7 @@ public: static bool GetIsLineOfSightSectorClear(CSector §or, const CColLine &line, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies, bool ignoreSeeThrough, bool ignoreSomeObjects = false); static bool GetIsLineOfSightSectorListClear(CPtrList &list, const CColLine &line, bool ignoreSeeThrough, bool ignoreSomeObjects = false); - static CEntity *TestSphereAgainstWorld(CVector centre, float distance, CEntity *entityToIgnore, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies, bool ignoreSomeObjects); + static CEntity *TestSphereAgainstWorld(CVector centre, float radius, CEntity *entityToIgnore, bool checkBuildings, bool checkVehicles, bool checkPeds, bool checkObjects, bool checkDummies, bool ignoreSomeObjects); static CEntity *TestSphereAgainstSectorList(CPtrList&, CVector, float, CEntity*, bool); static void FindObjectsInRangeSectorList(CPtrList&, CVector&, float, bool, short*, short, CEntity**); static void FindObjectsInRange(CVector&, float, bool, short*, short, CEntity**, bool, bool, bool, bool, bool); @@ -141,6 +139,8 @@ public: static void Process(); }; +extern CColPoint *gaTempSphereColPoints; + class CPlayerPed; class CVehicle; CPlayerPed *FindPlayerPed(void); diff --git a/src/core/config.h b/src/core/config.h index 9235e744..eb8d41c7 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -201,3 +201,5 @@ enum Config { #define VC_PED_PORTS // various ports from VC's CPed, mostly subtle #define NEW_WALK_AROUND_ALGORITHM // to make walking around vehicles/objects less awkward #define CANCELLABLE_CAR_ENTER + +#define IMPROVED_CAMERA // Better Debug cam, and maybe more in the future diff --git a/src/core/main.cpp b/src/core/main.cpp index 2a15e20e..663b09da 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -325,6 +325,7 @@ DoRWStuffStartOfFrame_Horizon(int16 TopRed, int16 TopGreen, int16 TopBlue, int16 void DoRWStuffEndOfFrame(void) { + CDebug::DisplayScreenStrings(); // custom CDebug::DebugDisplayTextBuffer(); // FlushObrsPrintfs(); RwCameraEndUpdate(Scene.camera); diff --git a/src/core/re3.cpp b/src/core/re3.cpp index 6f0a4682..ae64913e 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -20,6 +20,7 @@ #include "debugmenu_public.h" #include "Particle.h" #include "Console.h" +#include "Debug.h" #include #include @@ -114,13 +115,16 @@ SpawnCar(int id) CStreaming::LoadAllRequestedModels(false); if(CStreaming::HasModelLoaded(id)){ playerpos = FindPlayerCoors(); - int node = ThePaths.FindNodeClosestToCoors(playerpos, 0, 100.0f, false, false); - if(node < 0) - return; + int node; + if(!CModelInfo::IsBoatModel(id)){ + node = ThePaths.FindNodeClosestToCoors(playerpos, 0, 100.0f, false, false); + if(node < 0) + return; + } CVehicle *v; if(CModelInfo::IsBoatModel(id)) - return; + v = new CBoat(id, RANDOM_VEHICLE); else v = new CAutomobile(id, RANDOM_VEHICLE); @@ -130,7 +134,11 @@ SpawnCar(int id) if(carCol2) DebugMenuEntrySetAddress(carCol2, &v->m_currentColour2); - v->GetPosition() = ThePaths.m_pathNodes[node].pos; + if(CModelInfo::IsBoatModel(id)) + v->GetPosition() = TheCamera.GetPosition() + TheCamera.GetForward()*15.0f; + else + v->GetPosition() = ThePaths.m_pathNodes[node].pos; + v->GetPosition().z += 4.0f; v->SetOrientation(0.0f, 0.0f, 3.49f); v->m_status = STATUS_ABANDONED; @@ -197,6 +205,12 @@ PlaceOnRoad(void) ((CAutomobile*)veh)->PlaceOnRoadProperly(); } +static void +ResetCamStatics(void) +{ + TheCamera.Cams[TheCamera.ActiveCam].ResetStatics = true; +} + static const char *carnames[] = { "landstal", "idaho", "stinger", "linerun", "peren", "sentinel", "patriot", "firetruk", "trash", "stretch", "manana", "infernus", "blista", "pony", "mule", "cheetah", "ambulan", "fbicar", "moonbeam", "esperant", "taxi", "kuruma", "bobcat", "mrwhoop", "bfinject", "corpse", "police", "enforcer", @@ -358,7 +372,13 @@ DebugMenuPopulate(void) DebugMenuAddCmd("Debug", "Start Credits", CCredits::Start); DebugMenuAddCmd("Debug", "Stop Credits", CCredits::Stop); - + + extern bool PrintDebugCode; + extern int16 &DebugCamMode; + DebugMenuAddVarBool8("Cam", "Print Debug Code", (int8*)&PrintDebugCode, nil); + DebugMenuAddVar("Cam", "Cam Mode", &DebugCamMode, nil, 1, 0, CCam::MODE_EDITOR, nil); + DebugMenuAddCmd("Cam", "Reset Statics", ResetCamStatics); + CTweakVars::AddDBG("Debug"); } } @@ -433,7 +453,8 @@ void re3_debug(const char *format, ...) vsprintf_s(re3_buff, re3_buffsize, format, va); va_end(va); - printf("%s", re3_buff); +// printf("%s", re3_buff); + CDebug::DebugAddText(re3_buff); } void re3_trace(const char *filename, unsigned int lineno, const char *func, const char *format, ...) diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index db6b7ee2..b069afca 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -12169,11 +12169,11 @@ CPed::PlacePedOnDryLand(void) if (!CWorld::TestSphereAgainstWorld(potentialGround, 5.0f, nil, true, false, false, false, false, false)) return false; - CVector potentialGroundDist = CWorld::ms_testSpherePoint.point - GetPosition(); + CVector potentialGroundDist = gaTempSphereColPoints[0].point - GetPosition(); potentialGroundDist.z = 0.0f; potentialGroundDist.Normalise(); - CVector posToCheck = 0.5f * potentialGroundDist + CWorld::ms_testSpherePoint.point; + CVector posToCheck = 0.5f * potentialGroundDist + gaTempSphereColPoints[0].point; posToCheck.z = 3.0f + waterLevel; if (CWorld::ProcessVerticalLine(posToCheck, waterLevel - 1.0f, foundCol, foundEnt, true, true, false, true, false, false, false)) { diff --git a/src/render/Font.cpp b/src/render/Font.cpp index 7a16ad03..d7b4b5d8 100644 --- a/src/render/Font.cpp +++ b/src/render/Font.cpp @@ -94,7 +94,7 @@ CFont::Initialise(void) SetBackgroundColor(CRGBA(0x80, 0x80, 0x80, 0x80)); SetBackGroundOnlyTextOff(); SetPropOn(); - SetFontStyle(0); + SetFontStyle(FONT_BANK); SetRightJustifyWrap(0.0f); SetAlphaFade(255.0f); SetDropShadowPosition(0); diff --git a/src/render/Hud.cpp b/src/render/Hud.cpp index f0134062..52930067 100644 --- a/src/render/Hud.cpp +++ b/src/render/Hud.cpp @@ -115,47 +115,43 @@ void CHud::Draw() return; if (m_Wants_To_Draw_Hud && !TheCamera.m_WideScreenOn) { - bool Mode_RunAround = 0; - bool Mode_FirstPerson = 0; + bool DrawCrossHair = 0; + bool DrawCrossHairPC = 0; int32 WeaponType = FindPlayerPed()->m_weapons[FindPlayerPed()->m_currentWeapon].m_eWeaponType; int32 Mode = TheCamera.Cams[TheCamera.ActiveCam].Mode; - if (Mode == CCam::MODE_SNIPER || Mode == CCam::MODE_ROCKETLAUNCHER || Mode == CCam::MODE_M16_1STPERSON || Mode == CCam::MODE_EDITOR) - Mode_FirstPerson = 1; - if (Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || Mode == CCam::MODE_SNIPER_RUNABOUT) - Mode_RunAround = 1; + if (Mode == CCam::MODE_SNIPER || Mode == CCam::MODE_ROCKETLAUNCHER || Mode == CCam::MODE_M16_1STPERSON || Mode == CCam::MODE_HELICANNON_1STPERSON) + DrawCrossHair = 1; + if (Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || Mode == CCam::MODE_SNIPER_RUNABOUT) + DrawCrossHairPC = 1; /* Draw Crosshairs */ - if (TheCamera.Cams->Using3rdPersonMouseCam() && (!CPad::GetPad(0)->GetLookBehindForPed() || TheCamera.m_bPlayerIsInGarage) || Mode == CCam::MODE_1STPERSON_RUNABOUT) { + if (TheCamera.Cams[TheCamera.ActiveCam].Using3rdPersonMouseCam() && + (!CPad::GetPad(0)->GetLookBehindForPed() || TheCamera.m_bPlayerIsInGarage) || Mode == CCam::MODE_1STPERSON_RUNABOUT) { if (FindPlayerPed() && !FindPlayerPed()->EnteringCar()) { if ((WeaponType >= WEAPONTYPE_COLT45 && WeaponType <= WEAPONTYPE_M16) || WeaponType == WEAPONTYPE_FLAMETHROWER) - Mode_RunAround = 1; + DrawCrossHairPC = 1; } } - if (Mode_FirstPerson || Mode_RunAround) { + if (DrawCrossHair || DrawCrossHairPC) { RwRenderStateSet(rwRENDERSTATETEXTUREFILTER, (void *)rwFILTERLINEAR); - int32 SpriteBrightLikeADiamond = SpriteBrightness + 1; - if (SpriteBrightLikeADiamond > 30) - SpriteBrightLikeADiamond = 30; - - SpriteBrightness = SpriteBrightLikeADiamond; + SpriteBrightness = min(SpriteBrightness+1, 30); RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void*)FALSE); - float fStep = Sin((CTimer::GetTimeInMilliseconds() & 1023) * 0.0061328127); + float fStep = Sin((CTimer::GetTimeInMilliseconds() & 1023)/1024.0f * 6.28f); float fMultBright = SpriteBrightness * 0.03f * (0.25f * fStep + 0.75f); CRect rect; + if (DrawCrossHairPC && TheCamera.Cams[TheCamera.ActiveCam].Using3rdPersonMouseCam()) { #ifndef ASPECT_RATIO_SCALE - if (Mode_RunAround && TheCamera.Cams->Using3rdPersonMouseCam()) { float f3rdX = SCREEN_WIDTH * TheCamera.m_f3rdPersonCHairMultX; float f3rdY = SCREEN_HEIGHT * TheCamera.m_f3rdPersonCHairMultY; #else - if (Mode_RunAround && TheCamera.Cams->Using3rdPersonMouseCam()) { float f3rdX = (((TheCamera.m_f3rdPersonCHairMultX - 0.5f) / ((CDraw::GetAspectRatio()) / (DEFAULT_ASPECT_RATIO))) + 0.5f) * SCREEN_WIDTH; float f3rdY = SCREEN_HEIGHT * TheCamera.m_f3rdPersonCHairMultY + SCREEN_SCALE_Y(-2.0f); #endif @@ -179,14 +175,14 @@ void CHud::Draw() else { if (Mode == CCam::MODE_M16_1STPERSON || Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || - Mode == CCam::MODE_EDITOR) { + Mode == CCam::MODE_HELICANNON_1STPERSON) { rect.left = (SCREEN_WIDTH / 2) - SCREEN_SCALE_X(32.0f); rect.top = (SCREEN_HEIGHT / 2) - SCREEN_SCALE_Y(32.0f); rect.right = (SCREEN_WIDTH / 2) + SCREEN_SCALE_X(32.0f); rect.bottom = (SCREEN_HEIGHT / 2) + SCREEN_SCALE_Y(32.0f); Sprites[HUD_SITEM16].Draw(CRect(rect), CRGBA(255, 255, 255, 255)); } - else if (Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT) { + else if (Mode == CCam::MODE_1STPERSON_RUNABOUT) { rect.left = (SCREEN_WIDTH / 2) - SCREEN_SCALE_X(32.0f * 0.7f); rect.top = (SCREEN_HEIGHT / 2) - SCREEN_SCALE_Y(32.0f * 0.7f); rect.right = (SCREEN_WIDTH / 2) + SCREEN_SCALE_X(32.0f * 0.7f); @@ -194,17 +190,18 @@ void CHud::Draw() Sprites[HUD_SITEM16].Draw(CRect(rect), CRGBA(255, 255, 255, 255)); } - else if (Mode == CCam::MODE_ROCKETLAUNCHER || Mode == CCam::MODE_SNIPER_RUNABOUT) { + else if (Mode == CCam::MODE_ROCKETLAUNCHER || Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT) { RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)TRUE); RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDONE); RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDONE); RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void*)FALSE); RwRenderStateSet(rwRENDERSTATEZTESTENABLE, (void*)FALSE); - RwRenderStateSet(rwRENDERSTATETEXTURERASTER, gpRocketSightTex->raster); + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, RwTextureGetRaster(gpRocketSightTex)); CSprite::RenderOneXLUSprite(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, 1.0f, SCREEN_SCALE_X(40.0f), SCREEN_SCALE_Y(40.0f), (100.0f * fMultBright), (200.0f * fMultBright), (100.0f * fMultBright), 255, 1.0f, 255); } else { + // Sniper rect.left = (SCREEN_WIDTH / 2) - SCREEN_SCALE_X(210.0f); rect.top = (SCREEN_HEIGHT / 2) - SCREEN_SCALE_Y(210.0f); rect.right = SCREEN_WIDTH / 2; diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp index ff9f5755..d7834065 100644 --- a/src/render/Renderer.cpp +++ b/src/render/Renderer.cpp @@ -645,6 +645,9 @@ CRenderer::ScanWorld(void) m_loadingPriority = false; if(TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOPDOWN || +#ifdef FIX_BUGS + TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_GTACLASSIC || +#endif TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ CRect rect; int x1, x2, y1, y2; @@ -756,6 +759,9 @@ CRenderer::RequestObjectsInFrustum(void) RwV3dTransformPoints((RwV3d*)vectors, (RwV3d*)vectors, 9, cammatrix); if(TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOPDOWN || +#ifdef FIX_BUGS + TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_GTACLASSIC || +#endif TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ CRect rect; int x1, x2, y1, y2; From 22e022cc9f71bf08027ac37e17e5dafd5173858b Mon Sep 17 00:00:00 2001 From: aap Date: Fri, 27 Mar 2020 18:19:08 +0100 Subject: [PATCH 22/70] implemented some unused PS2 cams --- src/control/SceneEdit.cpp | 2 +- src/control/SceneEdit.h | 2 +- src/core/Cam.cpp | 285 +++++++++++++++++++++++++++++++++++++- src/core/Camera.h | 15 ++ src/core/re3.cpp | 4 + 5 files changed, 302 insertions(+), 6 deletions(-) diff --git a/src/control/SceneEdit.cpp b/src/control/SceneEdit.cpp index 8dec3435..4c05e11b 100644 --- a/src/control/SceneEdit.cpp +++ b/src/control/SceneEdit.cpp @@ -2,7 +2,7 @@ #include "patcher.h" #include "SceneEdit.h" -int &CSceneEdit::m_bCameraFollowActor = *(int*)0x940590; +int32 &CSceneEdit::m_bCameraFollowActor = *(int*)0x940590; bool &CSceneEdit::m_bRecording = *(bool*)0x95CD1F; CVector &CSceneEdit::m_vecCurrentPosition = *(CVector*)0x943064; CVector &CSceneEdit::m_vecCamHeading = *(CVector*)0x942F8C; diff --git a/src/control/SceneEdit.h b/src/control/SceneEdit.h index efcdb022..ec321b27 100644 --- a/src/control/SceneEdit.h +++ b/src/control/SceneEdit.h @@ -3,7 +3,7 @@ class CSceneEdit { public: - static int &m_bCameraFollowActor; + static int32 &m_bCameraFollowActor; static bool &m_bRecording; static CVector &m_vecCurrentPosition; static CVector &m_vecCamHeading; diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index 491a982c..12c72993 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -176,9 +176,15 @@ CCam::Process(void) case MODE_CAM_ON_A_STRING: Process_Cam_On_A_String(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; -// case MODE_REACTION: -// case MODE_FOLLOW_PED_WITH_BIND: -// case MODE_CHRIS: + case MODE_REACTION: + Process_ReactionCam(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_FOLLOW_PED_WITH_BIND: + Process_FollowPed_WithBinding(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; + case MODE_CHRIS: + Process_Chris_With_Binding_PlusRotation(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + break; case MODE_BEHINDBOAT: Process_BehindBoat(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; @@ -883,7 +889,7 @@ CCam::PrintMode(void) break; case MODE_REACTION: sprintf(buf, "Debug:- Cam Choice2. Reaction Cam On A String "); - sprintf(buf, " Uses Locking Button LeftShoulder 1. "); + sprintf(buf, " Uses Locking Button LeftShoulder 1. "); // lie break; case MODE_FOLLOW_PED_WITH_BIND: sprintf(buf, "Debug:- Cam Choice3. Game ReactionCam with Locking "); @@ -4092,6 +4098,277 @@ CCam::ProcessArrestCamTwo(void) return false; } + +/* + * Unused PS2 cams + */ + +void +CCam::Process_Chris_With_Binding_PlusRotation(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + static float AngleToBinned = 0.0f; + static float StartingAngleLastChange = 0.0f; + static float FixedTargetOrientation = 0.0f; + static float DeadZoneReachedOnePrevious; + + FOV = DefaultFOV; // missing in game + + bool FixOrientation = true; + if(ResetStatics){ + Rotating = false; + DeadZoneReachedOnePrevious = 0.0f; + FixedTargetOrientation = 0.0f; + ResetStatics = false; + } + + CVector TargetCoors = CameraTarget; + + float StickX = CPad::GetPad(0)->GetRightStickX(); + float StickY = CPad::GetPad(0)->GetRightStickY(); + float StickAngle; + if(StickX != 0.0 || StickY != 0.0f) // BUG: game checks StickX twice + StickAngle = CGeneral::GetATanOfXY(StickX, StickY); // result unused? + else + FixOrientation = false; + + CVector Dist = Source - TargetCoors; + Source.z = TargetCoors.z + 0.75f; + float Length = Dist.Magnitude2D(); + if(Length > 2.5f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.5f; + Source.y = TargetCoors.y + Dist.y/Length * 2.5f; + }else if(Length < 2.4f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.4f; + Source.y = TargetCoors.y + Dist.y/Length * 2.4f; + } + + Beta = CGeneral::GetATanOfXY(Dist.x, Dist.y); + if(CPad::GetPad(0)->GetLeftShoulder1()){ + FixedTargetOrientation = TargetOrientation; + Rotating = true; + } + + if(FixOrientation){ + Rotating = true; + FixedTargetOrientation = StickX/128.0f + Beta - PI; + } + + if(Rotating){ + Dist = Source - TargetCoors; + Length = Dist.Magnitude2D(); + // inlined + WellBufferMe(FixedTargetOrientation+PI, &Beta, &BetaSpeed, 0.1f, 0.06f, true); + + Source.x = TargetCoors.x + Length*Cos(Beta); + Source.y = TargetCoors.y + Length*Sin(Beta); + + float DeltaBeta = FixedTargetOrientation+PI - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < 0.06f) + Rotating = false; + } + + Front = TargetCoors - Source; + Front.Normalise(); + CVector Front2 = Front; + Front2.Normalise(); // What? + // FIX: the meaning of this value must have changed somehow + Source -= Front2 * TheCamera.m_fPedZoomValueSmooth*1.5f; +// Source += Front2 * TheCamera.m_fPedZoomValueSmooth; + + GetVectorsReadyForRW(); +} + +void +CCam::Process_ReactionCam(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + static float AngleToBinned = 0.0f; + static float StartingAngleLastChange = 0.0f; + static float FixedTargetOrientation; + static float DeadZoneReachedOnePrevious; + static uint32 TimeOfLastChange; + uint32 Time; + bool DontBind = false; // BUG: left uninitialized + + FOV = DefaultFOV; // missing in game + + if(ResetStatics){ + Rotating = false; + DeadZoneReachedOnePrevious = 0.0f; + FixedTargetOrientation = 0.0f; + ResetStatics = false; + DontBind = false; + } + + CVector TargetCoors = CameraTarget; + + CVector Dist = Source - TargetCoors; + Source.z = TargetCoors.z + 0.75f; + float Length = Dist.Magnitude2D(); + if(Length > 2.5f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.5f; + Source.y = TargetCoors.y + Dist.y/Length * 2.5f; + }else if(Length < 2.4f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.4f; + Source.y = TargetCoors.y + Dist.y/Length * 2.4f; + } + + Beta = CGeneral::GetATanOfXY(Dist.x, Dist.y); + + float StickX = CPad::GetPad(0)->GetLeftStickX(); + float StickY = CPad::GetPad(0)->GetLeftStickY(); + float StickAngle; + if(StickX != 0.0 || StickY != 0.0f){ + StickAngle = CGeneral::GetATanOfXY(StickX, StickY); + while(StickAngle >= PI) StickAngle -= 2*PI; + while(StickAngle < -PI) StickAngle += 2*PI; + }else + StickAngle = 1000.0f; + + if(Abs(StickAngle-AngleToBinned) > DEGTORAD(15.0f)){ + DontBind = true; + Time = CTimer::GetTimeInMilliseconds(); + } + + if(CTimer::GetTimeInMilliseconds()-TimeOfLastChange > 200){ + if(Abs(HALFPI-StickAngle) > DEGTORAD(50.0f)){ + FixedTargetOrientation = TargetOrientation; + Rotating = true; + TimeOfLastChange = CTimer::GetTimeInMilliseconds(); + } + } + + // These two together don't make much sense. + // Only prevents rotation for one frame + AngleToBinned = StickAngle; + if(DontBind) + TimeOfLastChange = Time; + + if(Rotating){ + Dist = Source - TargetCoors; + Length = Dist.Magnitude2D(); + // inlined + WellBufferMe(FixedTargetOrientation+PI, &Beta, &BetaSpeed, 0.1f, 0.06f, true); + + Source.x = TargetCoors.x + Length*Cos(Beta); + Source.y = TargetCoors.y + Length*Sin(Beta); + + float DeltaBeta = FixedTargetOrientation+PI - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < 0.06f) + Rotating = false; + } + + Front = TargetCoors - Source; + Front.Normalise(); + CVector Front2 = Front; + Front2.Normalise(); // What? + // FIX: the meaning of this value must have changed somehow + Source -= Front2 * TheCamera.m_fPedZoomValueSmooth*1.5f; +// Source += Front2 * TheCamera.m_fPedZoomValueSmooth; + + GetVectorsReadyForRW(); +} + +void +CCam::Process_FollowPed_WithBinding(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + static float AngleToBinned = 0.0f; + static float StartingAngleLastChange = 0.0f; + static float FixedTargetOrientation; + static float DeadZoneReachedOnePrevious; + static uint32 TimeOfLastChange; + uint32 Time; + bool DontBind = false; + + FOV = DefaultFOV; // missing in game + + if(ResetStatics){ + Rotating = false; + DeadZoneReachedOnePrevious = 0.0f; + FixedTargetOrientation = 0.0f; + ResetStatics = false; + } + + CVector TargetCoors = CameraTarget; + + CVector Dist = Source - TargetCoors; + Source.z = TargetCoors.z + 0.75f; + float Length = Dist.Magnitude2D(); + if(Length > 2.5f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.5f; + Source.y = TargetCoors.y + Dist.y/Length * 2.5f; + }else if(Length < 2.4f){ + Source.x = TargetCoors.x + Dist.x/Length * 2.4f; + Source.y = TargetCoors.y + Dist.y/Length * 2.4f; + } + + Beta = CGeneral::GetATanOfXY(Dist.x, Dist.y); + + float StickX = CPad::GetPad(0)->GetLeftStickX(); + float StickY = CPad::GetPad(0)->GetLeftStickY(); + float StickAngle; + if(StickX != 0.0 || StickY != 0.0f){ + StickAngle = CGeneral::GetATanOfXY(StickX, StickY); + while(StickAngle >= PI) StickAngle -= 2*PI; + while(StickAngle < -PI) StickAngle += 2*PI; + }else + StickAngle = 1000.0f; + + if(Abs(StickAngle-AngleToBinned) > DEGTORAD(15.0f)){ + DontBind = true; + Time = CTimer::GetTimeInMilliseconds(); + } + + if(CTimer::GetTimeInMilliseconds()-TimeOfLastChange > 200){ + if(Abs(HALFPI-StickAngle) > DEGTORAD(50.0f)){ + FixedTargetOrientation = TargetOrientation; + Rotating = true; + TimeOfLastChange = CTimer::GetTimeInMilliseconds(); + } + } + + if(CPad::GetPad(0)->GetLeftShoulder1JustDown()){ + FixedTargetOrientation = TargetOrientation; + Rotating = true; + TimeOfLastChange = CTimer::GetTimeInMilliseconds(); + } + + // These two together don't make much sense. + // Only prevents rotation for one frame + AngleToBinned = StickAngle; + if(DontBind) + TimeOfLastChange = Time; + + if(Rotating){ + Dist = Source - TargetCoors; + Length = Dist.Magnitude2D(); + // inlined + WellBufferMe(FixedTargetOrientation+PI, &Beta, &BetaSpeed, 0.1f, 0.06f, true); + + Source.x = TargetCoors.x + Length*Cos(Beta); + Source.y = TargetCoors.y + Length*Sin(Beta); + + float DeltaBeta = FixedTargetOrientation+PI - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < 0.06f) + Rotating = false; + } + + Front = TargetCoors - Source; + Front.Normalise(); + CVector Front2 = Front; + Front2.Normalise(); // What? + // FIX: the meaning of this value must have changed somehow + Source -= Front2 * TheCamera.m_fPedZoomValueSmooth*1.5f; +// Source += Front2 * TheCamera.m_fPedZoomValueSmooth; + + GetVectorsReadyForRW(); +} + STARTPATCHES InjectHook(0x456F40, WellBufferMe, PATCH_JUMP); InjectHook(0x458410, &CCam::Init, PATCH_JUMP); diff --git a/src/core/Camera.h b/src/core/Camera.h index 48f2d27a..982620a3 100644 --- a/src/core/Camera.h +++ b/src/core/Camera.h @@ -206,6 +206,21 @@ struct CCam void ProcessPedsDeadBaby(void); bool ProcessArrestCamOne(void); bool ProcessArrestCamTwo(void); + + /* Some of the unused PS2 cams */ + void Process_Chris_With_Binding_PlusRotation(const CVector &CameraTarget, float, float, float); + void Process_ReactionCam(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_FollowPed_WithBinding(const CVector &CameraTarget, float TargetOrientation, float, float); + // TODO: + // CCam::Process_CushyPillows_Arse + // CCam::Process_Look_At_Cars + // CCam::Process_CheesyZoom + // CCam::Process_Aiming + // CCam::Process_Bill // same as BehindCar due to unused variables + // CCam::Process_Im_The_Passenger_Woo_Woo + // CCam::Process_Blood_On_The_Tracks + // CCam::Process_Cam_Running_Side_Train + // CCam::Process_Cam_On_Train_Roof }; static_assert(sizeof(CCam) == 0x1A4, "CCam: wrong size"); static_assert(offsetof(CCam, Alpha) == 0xA8, "CCam: error"); diff --git a/src/core/re3.cpp b/src/core/re3.cpp index ae64913e..0301a98a 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -377,6 +377,10 @@ DebugMenuPopulate(void) extern int16 &DebugCamMode; DebugMenuAddVarBool8("Cam", "Print Debug Code", (int8*)&PrintDebugCode, nil); DebugMenuAddVar("Cam", "Cam Mode", &DebugCamMode, nil, 1, 0, CCam::MODE_EDITOR, nil); + DebugMenuAddCmd("Cam", "Normal", []() { DebugCamMode = 0; }); + DebugMenuAddCmd("Cam", "Follow Ped With Bind", []() { DebugCamMode = CCam::MODE_FOLLOW_PED_WITH_BIND; }); + DebugMenuAddCmd("Cam", "Reaction", []() { DebugCamMode = CCam::MODE_REACTION; }); + DebugMenuAddCmd("Cam", "Chris", []() { DebugCamMode = CCam::MODE_CHRIS; }); DebugMenuAddCmd("Cam", "Reset Statics", ResetCamStatics); CTweakVars::AddDBG("Debug"); From 43b092033cb7be208640b328bc42cdab7c5102ef Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Fri, 27 Mar 2020 20:54:35 +0200 Subject: [PATCH 23/70] XInput --- src/core/Pad.cpp | 56 ++++++++++++++++++++++++++++++++++++++++++++++- src/core/Pad.h | 4 ++++ src/core/config.h | 1 + 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/core/Pad.cpp b/src/core/Pad.cpp index 51102c7b..9a911aa4 100644 --- a/src/core/Pad.cpp +++ b/src/core/Pad.cpp @@ -5,6 +5,10 @@ #pragma warning( pop ) #include "common.h" +#ifdef XINPUT +#include +#pragma comment( lib, "Xinput.lib" ) +#endif #include "patcher.h" #include "Pad.h" #include "ControllerConfig.h" @@ -547,12 +551,60 @@ void CPad::AddToPCCheatString(char c) #undef _CHEATCMP } +#ifdef XINPUT +void CPad::AffectFromXinput(uint32 pad) +{ + XINPUT_STATE xstate; + memset(&xstate, 0, sizeof(XINPUT_STATE)); + if (XInputGetState(pad, &xstate) == ERROR_SUCCESS) + { + PCTempJoyState.Circle = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? 255 : 0; + PCTempJoyState.Cross = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? 255 : 0; + PCTempJoyState.Square = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? 255 : 0; + PCTempJoyState.Triangle = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? 255 : 0; + PCTempJoyState.DPadDown = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) ? 255 : 0; + PCTempJoyState.DPadLeft = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) ? 255 : 0; + PCTempJoyState.DPadRight = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) ? 255 : 0; + PCTempJoyState.DPadUp = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP) ? 255 : 0; + PCTempJoyState.LeftShock = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) ? 255 : 0; + PCTempJoyState.LeftShoulder1 = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? 255 : 0; + PCTempJoyState.LeftShoulder2 = xstate.Gamepad.bLeftTrigger; + PCTempJoyState.RightShock = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) ? 255 : 0; + PCTempJoyState.RightShoulder1 = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? 255 : 0; + PCTempJoyState.RightShoulder2 = xstate.Gamepad.bRightTrigger; + + PCTempJoyState.Select = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? 255 : 0; + PCTempJoyState.Start = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? 255 : 0; + + float lx = (float)xstate.Gamepad.sThumbLX / (float)0x7FFF; + float ly = (float)xstate.Gamepad.sThumbLY / (float)0x7FFF; + float rx = (float)xstate.Gamepad.sThumbRX / (float)0x7FFF; + float ry = (float)xstate.Gamepad.sThumbRY / (float)0x7FFF; + + if (Abs(lx) > 0.3f || Abs(ly) > 0.3f) { + PCTempJoyState.LeftStickX = (int32)(lx * 128.0f); + PCTempJoyState.LeftStickY = (int32)(-ly * 128.0f); + } + + if (Abs(rx) > 0.3f || Abs(ry) > 0.3f) { + PCTempJoyState.RightStickX = (int32)(rx * 128.0f); + PCTempJoyState.RightStickY = (int32)(ry * 128.0f); + } + } +} +#endif + void CPad::UpdatePads(void) { bool bUpdate = true; GetPad(0)->UpdateMouse(); +#ifdef XINPUT + GetPad(0)->AffectFromXinput(0); + GetPad(1)->AffectFromXinput(1); +#else CapturePad(0); +#endif ControlsManager.ClearSimButtonPressCheckers(); @@ -566,9 +618,11 @@ void CPad::UpdatePads(void) { GetPad(0)->Update(0); } - + +#if defined(MASTER) && !defined(XINPUT) GetPad(1)->NewState.Clear(); GetPad(1)->OldState.Clear(); +#endif OldKeyState = NewKeyState; NewKeyState = TempKeyState; diff --git a/src/core/Pad.h b/src/core/Pad.h index fec21df3..6cabdf54 100644 --- a/src/core/Pad.h +++ b/src/core/Pad.h @@ -247,6 +247,10 @@ public: static char *EditString(char *pStr, int32 nSize); static int32 *EditCodesForControls(int32 *pRsKeys, int32 nSize); +#ifdef XINPUT + void AffectFromXinput(uint32 pad); +#endif + // mouse bool GetLeftMouseJustDown() { return !!(NewMouseControllerState.LMB && !OldMouseControllerState.LMB); } bool GetRightMouseJustDown() { return !!(NewMouseControllerState.RMB && !OldMouseControllerState.RMB); } diff --git a/src/core/config.h b/src/core/config.h index 8b7b5a18..ba00992a 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -174,6 +174,7 @@ enum Config { #define TOGGLEABLE_BETA_FEATURES // toggleable from debug menu. not too many things // Pad +#define XINPUT #define KANGAROO_CHEAT #define REGISTER_START_BUTTON // currently only in menu sadly. resumes the game From e7c18fc17f82c40e937367726e07a58d5d4d7bce Mon Sep 17 00:00:00 2001 From: aap Date: Fri, 27 Mar 2020 20:53:47 +0100 Subject: [PATCH 24/70] removed windows.h for most .cpps --- rwsdk/include/d3d8/rwplcore.h | 4 +- src/control/Script.cpp | 1 + src/core/CdStream.cpp | 1 + src/core/CdStream.h | 1 - src/core/CutsceneMgr.cpp | 547 ++++++++++++++++---------------- src/core/General.h | 2 + src/core/Streaming.cpp | 2 +- src/core/Zones.cpp | 1 + src/core/common.h | 15 + src/core/patcher.h | 66 +--- src/core/re3.cpp | 51 +++ src/save/GenericGameStorage.cpp | 1 + src/save/PCSave.cpp | 1 + 13 files changed, 362 insertions(+), 331 deletions(-) diff --git a/rwsdk/include/d3d8/rwplcore.h b/rwsdk/include/d3d8/rwplcore.h index 152261cc..b0ff7dfa 100644 --- a/rwsdk/include/d3d8/rwplcore.h +++ b/rwsdk/include/d3d8/rwplcore.h @@ -3906,8 +3906,8 @@ MACRO_STOP #pragma warning( disable : 344 ) #endif /* (defined(__ICL)) */ - -#include +//nobody needed that - AAP +//#include #if (defined(RWDEBUG)) #if (defined(RWMEMDEBUG) && !defined(_CRTDBG_MAP_ALLOC)) diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 4aeacf3f..2cfd2a9b 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -1,3 +1,4 @@ +#define WITHWINDOWS // for our script loading hack #include "common.h" #include "patcher.h" diff --git a/src/core/CdStream.cpp b/src/core/CdStream.cpp index 57b1cbe2..a400c039 100644 --- a/src/core/CdStream.cpp +++ b/src/core/CdStream.cpp @@ -43,6 +43,7 @@ BOOL _gbCdStreamOverlapped; BOOL _gbCdStreamAsync; DWORD _gdwCdStreamFlags; +DWORD WINAPI CdStreamThread(LPVOID lpThreadParameter); void CdStreamInitThread(void) diff --git a/src/core/CdStream.h b/src/core/CdStream.h index 55507aa8..9ef71b65 100644 --- a/src/core/CdStream.h +++ b/src/core/CdStream.h @@ -39,7 +39,6 @@ int32 CdStreamSync(int32 channel); void AddToQueue(Queue *queue, int32 item); int32 GetFirstInQueue(Queue *queue); void RemoveFirstInQueue(Queue *queue); -DWORD WINAPI CdStreamThread(LPVOID lpThreadParameter); bool CdStreamAddImage(char const *path); char *CdStreamGetImageName(int32 cd); void CdStreamRemoveImages(void); diff --git a/src/core/CutsceneMgr.cpp b/src/core/CutsceneMgr.cpp index 3df81b2b..c13aa3a8 100644 --- a/src/core/CutsceneMgr.cpp +++ b/src/core/CutsceneMgr.cpp @@ -1,3 +1,4 @@ +#define WITHWINDOWS // just for VK_SPACE #include "common.h" #include "patcher.h" #include "General.h" @@ -27,79 +28,79 @@ const struct { { "BET", STREAMED_SOUND_BANK_INTRO }, { "L1_LG", STREAMED_SOUND_CUTSCENE_LUIGI1_LG }, { "L2_DSB", STREAMED_SOUND_CUTSCENE_LUIGI2_DSB }, - { "L3_DM", STREAMED_SOUND_CUTSCENE_LUIGI3_DM }, - { "L4_PAP", STREAMED_SOUND_CUTSCENE_LUIGI4_PAP }, - { "L5_TFB", STREAMED_SOUND_CUTSCENE_LUIGI5_TFB }, - { "J0_DM2", STREAMED_SOUND_CUTSCENE_JOEY0_DM2 }, - { "J1_LFL", STREAMED_SOUND_CUTSCENE_JOEY1_LFL }, - { "J2_KCL", STREAMED_SOUND_CUTSCENE_JOEY2_KCL }, - { "J3_VH", STREAMED_SOUND_CUTSCENE_JOEY3_VH }, - { "J4_ETH", STREAMED_SOUND_CUTSCENE_JOEY4_ETH }, - { "J5_DST", STREAMED_SOUND_CUTSCENE_JOEY5_DST }, - { "J6_TBJ", STREAMED_SOUND_CUTSCENE_JOEY6_TBJ }, - { "T1_TOL", STREAMED_SOUND_CUTSCENE_TONI1_TOL }, - { "T2_TPU", STREAMED_SOUND_CUTSCENE_TONI2_TPU }, - { "T3_MAS", STREAMED_SOUND_CUTSCENE_TONI3_MAS }, - { "T4_TAT", STREAMED_SOUND_CUTSCENE_TONI4_TAT }, - { "T5_BF", STREAMED_SOUND_CUTSCENE_TONI5_BF }, - { "S0_MAS", STREAMED_SOUND_CUTSCENE_SAL0_MAS }, - { "S1_PF", STREAMED_SOUND_CUTSCENE_SAL1_PF }, - { "S2_CTG", STREAMED_SOUND_CUTSCENE_SAL2_CTG }, - { "S3_RTC", STREAMED_SOUND_CUTSCENE_SAL3_RTC }, - { "S5_LRQ", STREAMED_SOUND_CUTSCENE_SAL5_LRQ }, - { "S4_BDBA", STREAMED_SOUND_CUTSCENE_SAL4_BDBA }, - { "S4_BDBB", STREAMED_SOUND_CUTSCENE_SAL4_BDBB }, - { "S2_CTG2", STREAMED_SOUND_CUTSCENE_SAL2_CTG2 }, - { "S4_BDBD", STREAMED_SOUND_CUTSCENE_SAL4_BDBD }, - { "S5_LRQB", STREAMED_SOUND_CUTSCENE_SAL5_LRQB }, - { "S5_LRQC", STREAMED_SOUND_CUTSCENE_SAL5_LRQC }, - { "A1_SS0", STREAMED_SOUND_CUTSCENE_ASUKA_1_SSO }, - { "A2_PP", STREAMED_SOUND_CUTSCENE_ASUKA_2_PP }, - { "A3_SS", STREAMED_SOUND_CUTSCENE_ASUKA_3_SS }, - { "A4_PDR", STREAMED_SOUND_CUTSCENE_ASUKA_4_PDR }, - { "A5_K2FT", STREAMED_SOUND_CUTSCENE_ASUKA_5_K2FT}, - { "K1_KBO", STREAMED_SOUND_CUTSCENE_KENJI1_KBO }, - { "K2_GIS", STREAMED_SOUND_CUTSCENE_KENJI2_GIS }, - { "K3_DS", STREAMED_SOUND_CUTSCENE_KENJI3_DS }, - { "K4_SHI", STREAMED_SOUND_CUTSCENE_KENJI4_SHI }, - { "K5_SD", STREAMED_SOUND_CUTSCENE_KENJI5_SD }, - { "R0_PDR2", STREAMED_SOUND_CUTSCENE_RAY0_PDR2 }, - { "R1_SW", STREAMED_SOUND_CUTSCENE_RAY1_SW }, - { "R2_AP", STREAMED_SOUND_CUTSCENE_RAY2_AP }, - { "R3_ED", STREAMED_SOUND_CUTSCENE_RAY3_ED }, - { "R4_GF", STREAMED_SOUND_CUTSCENE_RAY4_GF }, - { "R5_PB", STREAMED_SOUND_CUTSCENE_RAY5_PB }, - { "R6_MM", STREAMED_SOUND_CUTSCENE_RAY6_MM }, - { "D1_STOG", STREAMED_SOUND_CUTSCENE_DONALD1_STOG }, - { "D2_KK", STREAMED_SOUND_CUTSCENE_DONALD2_KK }, - { "D3_ADO", STREAMED_SOUND_CUTSCENE_DONALD3_ADO }, - { "D5_ES", STREAMED_SOUND_CUTSCENE_DONALD5_ES }, - { "D7_MLD", STREAMED_SOUND_CUTSCENE_DONALD7_MLD }, - { "D4_GTA", STREAMED_SOUND_CUTSCENE_DONALD4_GTA }, - { "D4_GTA2", STREAMED_SOUND_CUTSCENE_DONALD4_GTA2 }, - { "D6_STS", STREAMED_SOUND_CUTSCENE_DONALD6_STS }, - { "A6_BAIT", STREAMED_SOUND_CUTSCENE_ASUKA6_BAIT }, - { "A7_ETG", STREAMED_SOUND_CUTSCENE_ASUKA7_ETG }, - { "A8_PS", STREAMED_SOUND_CUTSCENE_ASUKA8_PS }, - { "A9_ASD", STREAMED_SOUND_CUTSCENE_ASUKA9_ASD }, - { "K4_SHI2", STREAMED_SOUND_CUTSCENE_KENJI4_SHI2 }, - { "C1_TEX", STREAMED_SOUND_CUTSCENE_CATALINA1_TEX }, - { "EL_PH1", STREAMED_SOUND_CUTSCENE_ELBURRO1_PH1 }, - { "EL_PH2", STREAMED_SOUND_CUTSCENE_ELBURRO2_PH2 }, - { "EL_PH3", STREAMED_SOUND_CUTSCENE_ELBURRO3_PH3 }, - { "EL_PH4", STREAMED_SOUND_CUTSCENE_ELBURRO4_PH4 }, - { "YD_PH1", STREAMED_SOUND_CUTSCENE_YARDIE_PH1 }, - { "YD_PH2", STREAMED_SOUND_CUTSCENE_YARDIE_PH2 }, - { "YD_PH3", STREAMED_SOUND_CUTSCENE_YARDIE_PH3 }, - { "YD_PH4", STREAMED_SOUND_CUTSCENE_YARDIE_PH4 }, - { "HD_PH1", STREAMED_SOUND_CUTSCENE_HOODS_PH1 }, - { "HD_PH2", STREAMED_SOUND_CUTSCENE_HOODS_PH2 }, - { "HD_PH3", STREAMED_SOUND_CUTSCENE_HOODS_PH3 }, - { "HD_PH4", STREAMED_SOUND_CUTSCENE_HOODS_PH4 }, - { "HD_PH5", STREAMED_SOUND_CUTSCENE_HOODS_PH5 }, - { "MT_PH1", STREAMED_SOUND_CUTSCENE_MARTY_PH1 }, - { "MT_PH2", STREAMED_SOUND_CUTSCENE_MARTY_PH2 }, - { "MT_PH3", STREAMED_SOUND_CUTSCENE_MARTY_PH3 }, + { "L3_DM", STREAMED_SOUND_CUTSCENE_LUIGI3_DM }, + { "L4_PAP", STREAMED_SOUND_CUTSCENE_LUIGI4_PAP }, + { "L5_TFB", STREAMED_SOUND_CUTSCENE_LUIGI5_TFB }, + { "J0_DM2", STREAMED_SOUND_CUTSCENE_JOEY0_DM2 }, + { "J1_LFL", STREAMED_SOUND_CUTSCENE_JOEY1_LFL }, + { "J2_KCL", STREAMED_SOUND_CUTSCENE_JOEY2_KCL }, + { "J3_VH", STREAMED_SOUND_CUTSCENE_JOEY3_VH }, + { "J4_ETH", STREAMED_SOUND_CUTSCENE_JOEY4_ETH }, + { "J5_DST", STREAMED_SOUND_CUTSCENE_JOEY5_DST }, + { "J6_TBJ", STREAMED_SOUND_CUTSCENE_JOEY6_TBJ }, + { "T1_TOL", STREAMED_SOUND_CUTSCENE_TONI1_TOL }, + { "T2_TPU", STREAMED_SOUND_CUTSCENE_TONI2_TPU }, + { "T3_MAS", STREAMED_SOUND_CUTSCENE_TONI3_MAS }, + { "T4_TAT", STREAMED_SOUND_CUTSCENE_TONI4_TAT }, + { "T5_BF", STREAMED_SOUND_CUTSCENE_TONI5_BF }, + { "S0_MAS", STREAMED_SOUND_CUTSCENE_SAL0_MAS }, + { "S1_PF", STREAMED_SOUND_CUTSCENE_SAL1_PF }, + { "S2_CTG", STREAMED_SOUND_CUTSCENE_SAL2_CTG }, + { "S3_RTC", STREAMED_SOUND_CUTSCENE_SAL3_RTC }, + { "S5_LRQ", STREAMED_SOUND_CUTSCENE_SAL5_LRQ }, + { "S4_BDBA", STREAMED_SOUND_CUTSCENE_SAL4_BDBA }, + { "S4_BDBB", STREAMED_SOUND_CUTSCENE_SAL4_BDBB }, + { "S2_CTG2", STREAMED_SOUND_CUTSCENE_SAL2_CTG2 }, + { "S4_BDBD", STREAMED_SOUND_CUTSCENE_SAL4_BDBD }, + { "S5_LRQB", STREAMED_SOUND_CUTSCENE_SAL5_LRQB }, + { "S5_LRQC", STREAMED_SOUND_CUTSCENE_SAL5_LRQC }, + { "A1_SS0", STREAMED_SOUND_CUTSCENE_ASUKA_1_SSO }, + { "A2_PP", STREAMED_SOUND_CUTSCENE_ASUKA_2_PP }, + { "A3_SS", STREAMED_SOUND_CUTSCENE_ASUKA_3_SS }, + { "A4_PDR", STREAMED_SOUND_CUTSCENE_ASUKA_4_PDR }, + { "A5_K2FT", STREAMED_SOUND_CUTSCENE_ASUKA_5_K2FT}, + { "K1_KBO", STREAMED_SOUND_CUTSCENE_KENJI1_KBO }, + { "K2_GIS", STREAMED_SOUND_CUTSCENE_KENJI2_GIS }, + { "K3_DS", STREAMED_SOUND_CUTSCENE_KENJI3_DS }, + { "K4_SHI", STREAMED_SOUND_CUTSCENE_KENJI4_SHI }, + { "K5_SD", STREAMED_SOUND_CUTSCENE_KENJI5_SD }, + { "R0_PDR2", STREAMED_SOUND_CUTSCENE_RAY0_PDR2 }, + { "R1_SW", STREAMED_SOUND_CUTSCENE_RAY1_SW }, + { "R2_AP", STREAMED_SOUND_CUTSCENE_RAY2_AP }, + { "R3_ED", STREAMED_SOUND_CUTSCENE_RAY3_ED }, + { "R4_GF", STREAMED_SOUND_CUTSCENE_RAY4_GF }, + { "R5_PB", STREAMED_SOUND_CUTSCENE_RAY5_PB }, + { "R6_MM", STREAMED_SOUND_CUTSCENE_RAY6_MM }, + { "D1_STOG", STREAMED_SOUND_CUTSCENE_DONALD1_STOG }, + { "D2_KK", STREAMED_SOUND_CUTSCENE_DONALD2_KK }, + { "D3_ADO", STREAMED_SOUND_CUTSCENE_DONALD3_ADO }, + { "D5_ES", STREAMED_SOUND_CUTSCENE_DONALD5_ES }, + { "D7_MLD", STREAMED_SOUND_CUTSCENE_DONALD7_MLD }, + { "D4_GTA", STREAMED_SOUND_CUTSCENE_DONALD4_GTA }, + { "D4_GTA2", STREAMED_SOUND_CUTSCENE_DONALD4_GTA2 }, + { "D6_STS", STREAMED_SOUND_CUTSCENE_DONALD6_STS }, + { "A6_BAIT", STREAMED_SOUND_CUTSCENE_ASUKA6_BAIT }, + { "A7_ETG", STREAMED_SOUND_CUTSCENE_ASUKA7_ETG }, + { "A8_PS", STREAMED_SOUND_CUTSCENE_ASUKA8_PS }, + { "A9_ASD", STREAMED_SOUND_CUTSCENE_ASUKA9_ASD }, + { "K4_SHI2", STREAMED_SOUND_CUTSCENE_KENJI4_SHI2 }, + { "C1_TEX", STREAMED_SOUND_CUTSCENE_CATALINA1_TEX }, + { "EL_PH1", STREAMED_SOUND_CUTSCENE_ELBURRO1_PH1 }, + { "EL_PH2", STREAMED_SOUND_CUTSCENE_ELBURRO2_PH2 }, + { "EL_PH3", STREAMED_SOUND_CUTSCENE_ELBURRO3_PH3 }, + { "EL_PH4", STREAMED_SOUND_CUTSCENE_ELBURRO4_PH4 }, + { "YD_PH1", STREAMED_SOUND_CUTSCENE_YARDIE_PH1 }, + { "YD_PH2", STREAMED_SOUND_CUTSCENE_YARDIE_PH2 }, + { "YD_PH3", STREAMED_SOUND_CUTSCENE_YARDIE_PH3 }, + { "YD_PH4", STREAMED_SOUND_CUTSCENE_YARDIE_PH4 }, + { "HD_PH1", STREAMED_SOUND_CUTSCENE_HOODS_PH1 }, + { "HD_PH2", STREAMED_SOUND_CUTSCENE_HOODS_PH2 }, + { "HD_PH3", STREAMED_SOUND_CUTSCENE_HOODS_PH3 }, + { "HD_PH4", STREAMED_SOUND_CUTSCENE_HOODS_PH4 }, + { "HD_PH5", STREAMED_SOUND_CUTSCENE_HOODS_PH5 }, + { "MT_PH1", STREAMED_SOUND_CUTSCENE_MARTY_PH1 }, + { "MT_PH2", STREAMED_SOUND_CUTSCENE_MARTY_PH2 }, + { "MT_PH3", STREAMED_SOUND_CUTSCENE_MARTY_PH3 }, { "MT_PH4", STREAMED_SOUND_CUTSCENE_MARTY_PH4 }, { NULL, NULL } }; @@ -128,135 +129,135 @@ CVector &CCutsceneMgr::ms_cutsceneOffset = *(CVector*)0x8F2C0C; float &CCutsceneMgr::ms_cutsceneTimer = *(float*)0x941548; uint32 &CCutsceneMgr::ms_cutsceneLoadStatus = *(uint32*)0x95CB40; -RpAtomic * -CalculateBoundingSphereRadiusCB(RpAtomic *atomic, void *data) -{ - float radius = RpAtomicGetBoundingSphereMacro(atomic)->radius; - RwV3d center = RpAtomicGetBoundingSphereMacro(atomic)->center; - - for (RwFrame *frame = RpAtomicGetFrame(atomic); RwFrameGetParent(frame); frame = RwFrameGetParent(frame)) - RwV3dTransformPoints(¢er, ¢er, 1, RwFrameGetMatrix(frame)); - - float size = RwV3dLength(¢er) + radius; - if (size > *(float *)data) - *(float *)data = size; - return atomic; +RpAtomic * +CalculateBoundingSphereRadiusCB(RpAtomic *atomic, void *data) +{ + float radius = RpAtomicGetBoundingSphereMacro(atomic)->radius; + RwV3d center = RpAtomicGetBoundingSphereMacro(atomic)->center; + + for (RwFrame *frame = RpAtomicGetFrame(atomic); RwFrameGetParent(frame); frame = RwFrameGetParent(frame)) + RwV3dTransformPoints(¢er, ¢er, 1, RwFrameGetMatrix(frame)); + + float size = RwV3dLength(¢er) + radius; + if (size > *(float *)data) + *(float *)data = size; + return atomic; } void CCutsceneMgr::Initialise(void) -{ - ms_numCutsceneObjs = 0; - ms_loaded = false; - ms_running = false; - ms_animLoaded = false; - ms_cutsceneProcessing = false; - ms_useLodMultiplier = false; - - ms_pCutsceneDir = new CDirectory(CUTSCENEDIRSIZE); +{ + ms_numCutsceneObjs = 0; + ms_loaded = false; + ms_running = false; + ms_animLoaded = false; + ms_cutsceneProcessing = false; + ms_useLodMultiplier = false; + + ms_pCutsceneDir = new CDirectory(CUTSCENEDIRSIZE); ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR"); } -void -CCutsceneMgr::Shutdown(void) -{ - delete ms_pCutsceneDir; +void +CCutsceneMgr::Shutdown(void) +{ + delete ms_pCutsceneDir; } -void -CCutsceneMgr::LoadCutsceneData(const char *szCutsceneName) -{ - int file; - uint32 size; - uint32 offset; - CPlayerPed *pPlayerPed; - - ms_cutsceneProcessing = true; - if (!strcasecmp(szCutsceneName, "jb")) - ms_useLodMultiplier = true; - CTimer::Stop(); - - ms_pCutsceneDir->numEntries = 0; - ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR"); - - CStreaming::RemoveUnusedModelsInLoadedList(); - CGame::DrasticTidyUpMemory(); - - strcpy(ms_cutsceneName, szCutsceneName); - file = CFileMgr::OpenFile("ANIM\\CUTS.IMG", "rb"); - - // Load animations - sprintf(gString, "%s.IFP", szCutsceneName); - if (ms_pCutsceneDir->FindItem(gString, offset, size)) { - CStreaming::MakeSpaceFor(size << 11); - CStreaming::ImGonnaUseStreamingMemory(); - CFileMgr::Seek(file, offset << 11, SEEK_SET); - CAnimManager::LoadAnimFile(file, false); - ms_cutsceneAssociations.CreateAssociations(szCutsceneName); - CStreaming::IHaveUsedStreamingMemory(); - ms_animLoaded = true; - } else { - ms_animLoaded = false; - } - - // Load camera data - sprintf(gString, "%s.DAT", szCutsceneName); - if (ms_pCutsceneDir->FindItem(gString, offset, size)) { - CFileMgr::Seek(file, offset << 11, SEEK_SET); - TheCamera.LoadPathSplines(file); - } - - CFileMgr::CloseFile(file); - - if (CGeneral::faststricmp(ms_cutsceneName, "end")) { - DMAudio.ChangeMusicMode(MUSICMODE_CUTSCENE); - int trackId = FindCutsceneAudioTrackId(szCutsceneName); - if (trackId != -1) { - printf("Start preload audio %s\n", szCutsceneName); - DMAudio.PreloadCutSceneMusic(trackId); - printf("End preload audio %s\n", szCutsceneName); - } - } - - ms_cutsceneTimer = 0.0f; - ms_loaded = true; - ms_cutsceneOffset = CVector(0.0f, 0.0f, 0.0f); - - pPlayerPed = FindPlayerPed(); - CTimer::Update(); - - pPlayerPed->m_pWanted->ClearQdCrimes(); - pPlayerPed->bIsVisible = false; - pPlayerPed->m_fCurrentStamina = pPlayerPed->m_fMaxStamina; - CPad::GetPad(0)->DisablePlayerControls |= PLAYERCONTROL_DISABLED_80; - CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(true); +void +CCutsceneMgr::LoadCutsceneData(const char *szCutsceneName) +{ + int file; + uint32 size; + uint32 offset; + CPlayerPed *pPlayerPed; + + ms_cutsceneProcessing = true; + if (!strcasecmp(szCutsceneName, "jb")) + ms_useLodMultiplier = true; + CTimer::Stop(); + + ms_pCutsceneDir->numEntries = 0; + ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR"); + + CStreaming::RemoveUnusedModelsInLoadedList(); + CGame::DrasticTidyUpMemory(); + + strcpy(ms_cutsceneName, szCutsceneName); + file = CFileMgr::OpenFile("ANIM\\CUTS.IMG", "rb"); + + // Load animations + sprintf(gString, "%s.IFP", szCutsceneName); + if (ms_pCutsceneDir->FindItem(gString, offset, size)) { + CStreaming::MakeSpaceFor(size << 11); + CStreaming::ImGonnaUseStreamingMemory(); + CFileMgr::Seek(file, offset << 11, SEEK_SET); + CAnimManager::LoadAnimFile(file, false); + ms_cutsceneAssociations.CreateAssociations(szCutsceneName); + CStreaming::IHaveUsedStreamingMemory(); + ms_animLoaded = true; + } else { + ms_animLoaded = false; + } + + // Load camera data + sprintf(gString, "%s.DAT", szCutsceneName); + if (ms_pCutsceneDir->FindItem(gString, offset, size)) { + CFileMgr::Seek(file, offset << 11, SEEK_SET); + TheCamera.LoadPathSplines(file); + } + + CFileMgr::CloseFile(file); + + if (CGeneral::faststricmp(ms_cutsceneName, "end")) { + DMAudio.ChangeMusicMode(MUSICMODE_CUTSCENE); + int trackId = FindCutsceneAudioTrackId(szCutsceneName); + if (trackId != -1) { + printf("Start preload audio %s\n", szCutsceneName); + DMAudio.PreloadCutSceneMusic(trackId); + printf("End preload audio %s\n", szCutsceneName); + } + } + + ms_cutsceneTimer = 0.0f; + ms_loaded = true; + ms_cutsceneOffset = CVector(0.0f, 0.0f, 0.0f); + + pPlayerPed = FindPlayerPed(); + CTimer::Update(); + + pPlayerPed->m_pWanted->ClearQdCrimes(); + pPlayerPed->bIsVisible = false; + pPlayerPed->m_fCurrentStamina = pPlayerPed->m_fMaxStamina; + CPad::GetPad(0)->DisablePlayerControls |= PLAYERCONTROL_DISABLED_80; + CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(true); } -void -CCutsceneMgr::SetHeadAnim(const char *animName, CObject *pObject) -{ - CCutsceneHead *pCutsceneHead = (CCutsceneHead*)pObject; - char szAnim[CUTSCENENAMESIZE * 2]; - - sprintf(szAnim, "%s_%s", ms_cutsceneName, animName); - pCutsceneHead->PlayAnimation(szAnim); +void +CCutsceneMgr::SetHeadAnim(const char *animName, CObject *pObject) +{ + CCutsceneHead *pCutsceneHead = (CCutsceneHead*)pObject; + char szAnim[CUTSCENENAMESIZE * 2]; + + sprintf(szAnim, "%s_%s", ms_cutsceneName, animName); + pCutsceneHead->PlayAnimation(szAnim); } -void -CCutsceneMgr::FinishCutscene() -{ - CCutsceneMgr::ms_cutsceneTimer = TheCamera.GetCutSceneFinishTime() * 0.001f; - TheCamera.FinishCutscene(); - - FindPlayerPed()->bIsVisible = true; - CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false); +void +CCutsceneMgr::FinishCutscene() +{ + CCutsceneMgr::ms_cutsceneTimer = TheCamera.GetCutSceneFinishTime() * 0.001f; + TheCamera.FinishCutscene(); + + FindPlayerPed()->bIsVisible = true; + CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false); } void CCutsceneMgr::SetupCutsceneToStart(void) { - TheCamera.SetCamCutSceneOffSet(ms_cutsceneOffset); - TheCamera.TakeControlWithSpline(JUMP_CUT); + TheCamera.SetCamCutSceneOffSet(ms_cutsceneOffset); + TheCamera.TakeControlWithSpline(JUMP_CUT); TheCamera.SetWideScreenOn(); ms_cutsceneOffset.z++; @@ -273,9 +274,9 @@ CCutsceneMgr::SetupCutsceneToStart(void) } } - CTimer::Update(); - CTimer::Update(); - ms_running = true; + CTimer::Update(); + CTimer::Update(); + ms_running = true; ms_cutsceneTimer = 0.0f; } @@ -297,14 +298,14 @@ CCutsceneMgr::SetCutsceneAnim(const char *animName, CObject *pObject) pAnimBlendClumpData->link.Prepend(&pNewAnim->link); } -CCutsceneHead * -CCutsceneMgr::AddCutsceneHead(CObject *pObject, int modelId) -{ - CCutsceneHead *pHead = new CCutsceneHead(pObject); - pHead->SetModelIndex(modelId); - CWorld::Add(pHead); - ms_pCutsceneObjects[ms_numCutsceneObjs++] = pHead; - return pHead; +CCutsceneHead * +CCutsceneMgr::AddCutsceneHead(CObject *pObject, int modelId) +{ + CCutsceneHead *pHead = new CCutsceneHead(pObject); + pHead->SetModelIndex(modelId); + CWorld::Add(pHead); + ms_pCutsceneObjects[ms_numCutsceneObjs++] = pHead; + return pHead; } CCutsceneObject * @@ -333,89 +334,89 @@ CCutsceneMgr::CreateCutsceneObject(int modelId) pCutsceneObject = new CCutsceneObject(); pCutsceneObject->SetModelIndex(modelId); - ms_pCutsceneObjects[ms_numCutsceneObjs++] = pCutsceneObject; + ms_pCutsceneObjects[ms_numCutsceneObjs++] = pCutsceneObject; return pCutsceneObject; } -void -CCutsceneMgr::DeleteCutsceneData(void) -{ - if (!ms_loaded) return; - - ms_cutsceneProcessing = false; - ms_useLodMultiplier = false; - - for (--ms_numCutsceneObjs; ms_numCutsceneObjs >= 0; ms_numCutsceneObjs--) { - CWorld::Remove(ms_pCutsceneObjects[ms_numCutsceneObjs]); - ms_pCutsceneObjects[ms_numCutsceneObjs]->DeleteRwObject(); - delete ms_pCutsceneObjects[ms_numCutsceneObjs]; - } - ms_numCutsceneObjs = 0; - - if (ms_animLoaded) - CAnimManager::RemoveLastAnimFile(); - - ms_animLoaded = false; - TheCamera.RestoreWithJumpCut(); - TheCamera.SetWideScreenOff(); - ms_running = false; - ms_loaded = false; - - FindPlayerPed()->bIsVisible = true; - CPad::GetPad(0)->DisablePlayerControls &= ~PLAYERCONTROL_DISABLED_80; - CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false); - - if (CGeneral::faststricmp(ms_cutsceneName, "end")) { - DMAudio.StopCutSceneMusic(); - if (CGeneral::faststricmp(ms_cutsceneName, "bet")) - DMAudio.ChangeMusicMode(MUSICMODE_GAME); - } - CTimer::Stop(); - //TheCamera.GetScreenFadeStatus() == 2; // what for?? - CGame::DrasticTidyUpMemory(); - CTimer::Update(); +void +CCutsceneMgr::DeleteCutsceneData(void) +{ + if (!ms_loaded) return; + + ms_cutsceneProcessing = false; + ms_useLodMultiplier = false; + + for (--ms_numCutsceneObjs; ms_numCutsceneObjs >= 0; ms_numCutsceneObjs--) { + CWorld::Remove(ms_pCutsceneObjects[ms_numCutsceneObjs]); + ms_pCutsceneObjects[ms_numCutsceneObjs]->DeleteRwObject(); + delete ms_pCutsceneObjects[ms_numCutsceneObjs]; + } + ms_numCutsceneObjs = 0; + + if (ms_animLoaded) + CAnimManager::RemoveLastAnimFile(); + + ms_animLoaded = false; + TheCamera.RestoreWithJumpCut(); + TheCamera.SetWideScreenOff(); + ms_running = false; + ms_loaded = false; + + FindPlayerPed()->bIsVisible = true; + CPad::GetPad(0)->DisablePlayerControls &= ~PLAYERCONTROL_DISABLED_80; + CWorld::Players[CWorld::PlayerInFocus].MakePlayerSafe(false); + + if (CGeneral::faststricmp(ms_cutsceneName, "end")) { + DMAudio.StopCutSceneMusic(); + if (CGeneral::faststricmp(ms_cutsceneName, "bet")) + DMAudio.ChangeMusicMode(MUSICMODE_GAME); + } + CTimer::Stop(); + //TheCamera.GetScreenFadeStatus() == 2; // what for?? + CGame::DrasticTidyUpMemory(); + CTimer::Update(); } -void -CCutsceneMgr::Update(void) -{ - enum { - CUTSCENE_LOADING_0 = 0, - CUTSCENE_LOADING_AUDIO, - CUTSCENE_LOADING_2, - CUTSCENE_LOADING_3, - CUTSCENE_LOADING_4 - }; - - switch (ms_cutsceneLoadStatus) { - case CUTSCENE_LOADING_AUDIO: - SetupCutsceneToStart(); - if (CGeneral::faststricmp(ms_cutsceneName, "end")) - DMAudio.PlayPreloadedCutSceneMusic(); - ms_cutsceneLoadStatus++; - break; - case CUTSCENE_LOADING_2: - case CUTSCENE_LOADING_3: - ms_cutsceneLoadStatus++; - break; - case CUTSCENE_LOADING_4: - ms_cutsceneLoadStatus = CUTSCENE_LOADING_0; - break; - default: - break; - } - - if (!ms_running) return; - - ms_cutsceneTimer += CTimer::GetTimeStepNonClipped() * 0.02f; - if (CGeneral::faststricmp(ms_cutsceneName, "end") && TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_FLYBY && ms_cutsceneLoadStatus == CUTSCENE_LOADING_0) { - if (CPad::GetPad(0)->GetCrossJustDown() - || (CGame::playingIntro && CPad::GetPad(0)->GetStartJustDown()) - || CPad::GetPad(0)->GetLeftMouseJustDown() - || CPad::GetPad(0)->GetEnterJustDown() - || CPad::GetPad(0)->GetCharJustDown(VK_SPACE)) - FinishCutscene(); - } +void +CCutsceneMgr::Update(void) +{ + enum { + CUTSCENE_LOADING_0 = 0, + CUTSCENE_LOADING_AUDIO, + CUTSCENE_LOADING_2, + CUTSCENE_LOADING_3, + CUTSCENE_LOADING_4 + }; + + switch (ms_cutsceneLoadStatus) { + case CUTSCENE_LOADING_AUDIO: + SetupCutsceneToStart(); + if (CGeneral::faststricmp(ms_cutsceneName, "end")) + DMAudio.PlayPreloadedCutSceneMusic(); + ms_cutsceneLoadStatus++; + break; + case CUTSCENE_LOADING_2: + case CUTSCENE_LOADING_3: + ms_cutsceneLoadStatus++; + break; + case CUTSCENE_LOADING_4: + ms_cutsceneLoadStatus = CUTSCENE_LOADING_0; + break; + default: + break; + } + + if (!ms_running) return; + + ms_cutsceneTimer += CTimer::GetTimeStepNonClipped() * 0.02f; + if (CGeneral::faststricmp(ms_cutsceneName, "end") && TheCamera.Cams[TheCamera.ActiveCam].Mode == CCam::MODE_FLYBY && ms_cutsceneLoadStatus == CUTSCENE_LOADING_0) { + if (CPad::GetPad(0)->GetCrossJustDown() + || (CGame::playingIntro && CPad::GetPad(0)->GetStartJustDown()) + || CPad::GetPad(0)->GetLeftMouseJustDown() + || CPad::GetPad(0)->GetEnterJustDown() + || CPad::GetPad(0)->GetCharJustDown(VK_SPACE)) + FinishCutscene(); + } } bool CCutsceneMgr::HasCutsceneFinished(void) { return TheCamera.GetPositionAlongSpline() == 1.0f; } diff --git a/src/core/General.h b/src/core/General.h index a7b240c2..f32846eb 100644 --- a/src/core/General.h +++ b/src/core/General.h @@ -1,5 +1,7 @@ #pragma once +#include + class CGeneral { public: diff --git a/src/core/Streaming.cpp b/src/core/Streaming.cpp index 3a830d37..6106f3df 100644 --- a/src/core/Streaming.cpp +++ b/src/core/Streaming.cpp @@ -198,7 +198,7 @@ CStreaming::Init(void) // PC only, figure out how much memory we got #ifdef GTA_PC #define MB (1024*1024) - extern DWORD &_dwMemAvailPhys; + extern unsigned long &_dwMemAvailPhys; ms_memoryAvailable = (_dwMemAvailPhys - 10*MB)/2; if(ms_memoryAvailable < 50*MB) ms_memoryAvailable = 50*MB; diff --git a/src/core/Zones.cpp b/src/core/Zones.cpp index 363fc3d9..4bce3e79 100644 --- a/src/core/Zones.cpp +++ b/src/core/Zones.cpp @@ -1,5 +1,6 @@ #include "common.h" #include "patcher.h" +#include #include "Zones.h" diff --git a/src/core/common.h b/src/core/common.h index 3127cb12..0cdff871 100644 --- a/src/core/common.h +++ b/src/core/common.h @@ -8,10 +8,15 @@ #pragma warning(disable: 4996) // POSIX names #include +#include #include //#include #include +#ifdef WITHWINDOWS +#include +#endif + #ifdef WITHD3D #include #include @@ -30,6 +35,16 @@ #undef near #endif +#ifndef max +#define max(a,b) ((a) > (b) ? (a) : (b)) +#endif +#ifndef min +#define min(a,b) ((a) < (b) ? (a) : (b)) +#endif +#ifndef ARRAYSIZE +#define ARRAYSIZE(a) (sizeof(a) / sizeof(*(a))) +#endif + typedef uint8_t uint8; typedef int8_t int8; typedef uint16_t uint16; diff --git a/src/core/patcher.h b/src/core/patcher.h index 87a6bea4..3dfbb05c 100644 --- a/src/core/patcher.h +++ b/src/core/patcher.h @@ -6,13 +6,7 @@ #define VARJMP(a) { _asm jmp a } #define WRAPARG(a) UNREFERENCED_PARAMETER(a) -#define NOVMT __declspec(novtable) -#define SETVMT(a) *((DWORD_PTR*)this) = (DWORD_PTR)a - -#include -#include - -#include "common.h" +#include //memset enum { @@ -103,72 +97,36 @@ isVC(void) InjectHook(a, func); \ } +void InjectHook_internal(uint32 address, uint32 hook, int type); +void Protect_internal(uint32 address, uint32 size); +void Unprotect_internal(void); + template inline void Patch(AT address, T value) { - DWORD dwProtect[2]; - VirtualProtect((void*)address, sizeof(T), PAGE_EXECUTE_READWRITE, &dwProtect[0]); + Protect_internal((uint32)address, sizeof(T)); *(T*)address = value; - VirtualProtect((void*)address, sizeof(T), dwProtect[0], &dwProtect[1]); + Unprotect_internal(); } template inline void Nop(AT address, unsigned int nCount) { - DWORD dwProtect[2]; - VirtualProtect((void*)address, nCount, PAGE_EXECUTE_READWRITE, &dwProtect[0]); + Protect_internal((uint32)address, nCount); memset((void*)address, 0x90, nCount); - VirtualProtect((void*)address, nCount, dwProtect[0], &dwProtect[1]); + Unprotect_internal(); } -template inline void -ClearCC(AT address, unsigned int nCount) -{ - DWORD dwProtect[2]; - VirtualProtect((void*)address, nCount, PAGE_EXECUTE_READWRITE, &dwProtect[0]); - memset((void*)address, 0xCC, nCount); - VirtualProtect((void*)address, nCount, dwProtect[0], &dwProtect[1]); -} - -extern std::vector usedAddresses; - template inline void InjectHook(AT address, HT hook, unsigned int nType=PATCH_NOTHING) { - if(std::any_of(usedAddresses.begin(), usedAddresses.end(), - [address](AT value) { return (int32)value == address; })) { - debug("Used address %#06x twice when injecting hook\n", address); - } - - usedAddresses.push_back((int32)address); - - DWORD dwProtect[2]; - switch ( nType ) - { - case PATCH_JUMP: - VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &dwProtect[0]); - *(BYTE*)address = 0xE9; - break; - case PATCH_CALL: - VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &dwProtect[0]); - *(BYTE*)address = 0xE8; - break; - default: - VirtualProtect((void*)((DWORD)address + 1), 4, PAGE_EXECUTE_READWRITE, &dwProtect[0]); - break; - } - DWORD dwHook; + uint32 uiHook; _asm { mov eax, hook - mov dwHook, eax + mov uiHook, eax } - - *(ptrdiff_t*)((DWORD)address + 1) = (DWORD)dwHook - (DWORD)address - 5; - if ( nType == PATCH_NOTHING ) - VirtualProtect((void*)((DWORD)address + 1), 4, dwProtect[0], &dwProtect[1]); - else - VirtualProtect((void*)address, 5, dwProtect[0], &dwProtect[1]); + InjectHook_internal((uint32)address, uiHook, nType); } inline void ExtractCall(void *dst, uint32_t a) diff --git a/src/core/re3.cpp b/src/core/re3.cpp index 0301a98a..137a890c 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -22,11 +22,62 @@ #include "Console.h" #include "Debug.h" +#include #include #include std::vector usedAddresses; +static DWORD protect[2]; +static uint32 protect_address; +static uint32 protect_size; + +void +Protect_internal(uint32 address, uint32 size) +{ + protect_address = address; + protect_size = size; + VirtualProtect((void*)address, size, PAGE_EXECUTE_READWRITE, &protect[0]); +} + +void +Unprotect_internal(void) +{ + VirtualProtect((void*)protect_address, protect_size, protect[0], &protect[1]); +} + +void +InjectHook_internal(uint32 address, uint32 hook, int type) +{ + if(std::any_of(usedAddresses.begin(), usedAddresses.end(), + [address](uint32 value) { return (int32)value == address; })) { + debug("Used address %#06x twice when injecting hook\n", address); + } + + usedAddresses.push_back((int32)address); + + + switch(type){ + case PATCH_JUMP: + VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); + *(uint8*)address = 0xE9; + break; + case PATCH_CALL: + VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); + *(uint8*)address = 0xE8; + break; + default: + VirtualProtect((void*)((uint32)address + 1), 4, PAGE_EXECUTE_READWRITE, &protect[0]); + break; + } + + *(ptrdiff_t*)(address + 1) = hook - address - 5; + if(type == PATCH_NOTHING) + VirtualProtect((void*)(address + 1), 4, protect[0], &protect[1]); + else + VirtualProtect((void*)address, 5, protect[0], &protect[1]); +} + void **rwengine = *(void***)0x5A10E1; DebugMenuAPI gDebugMenuAPI; diff --git a/src/save/GenericGameStorage.cpp b/src/save/GenericGameStorage.cpp index 5288e67e..2d5cfae2 100644 --- a/src/save/GenericGameStorage.cpp +++ b/src/save/GenericGameStorage.cpp @@ -1,3 +1,4 @@ +#define WITHWINDOWS #include "common.h" #include "main.h" #include "patcher.h" diff --git a/src/save/PCSave.cpp b/src/save/PCSave.cpp index e94db6db..744f5e0d 100644 --- a/src/save/PCSave.cpp +++ b/src/save/PCSave.cpp @@ -1,3 +1,4 @@ +#define WITHWINDOWS #include "common.h" #include "patcher.h" #include "FileMgr.h" From 184a80cc3b7ecd054f09ec5519fded5fb4efa162 Mon Sep 17 00:00:00 2001 From: Filip Gawin Date: Fri, 27 Mar 2020 21:20:28 +0100 Subject: [PATCH 25/70] Remove assembly from patcher.h --- src/core/patcher.h | 12 +++--------- src/core/re3.cpp | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/core/patcher.h b/src/core/patcher.h index 3dfbb05c..2722b6fd 100644 --- a/src/core/patcher.h +++ b/src/core/patcher.h @@ -117,16 +117,10 @@ Nop(AT address, unsigned int nCount) Unprotect_internal(); } -template inline void -InjectHook(AT address, HT hook, unsigned int nType=PATCH_NOTHING) +template inline void +InjectHook(uintptr_t address, T hook, unsigned int nType = PATCH_NOTHING) { - uint32 uiHook; - _asm - { - mov eax, hook - mov uiHook, eax - } - InjectHook_internal((uint32)address, uiHook, nType); + InjectHook_internal(address, reinterpret_cast((void *&)hook), nType); } inline void ExtractCall(void *dst, uint32_t a) diff --git a/src/core/re3.cpp b/src/core/re3.cpp index 137a890c..ffb2a7a2 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -71,7 +71,7 @@ InjectHook_internal(uint32 address, uint32 hook, int type) break; } - *(ptrdiff_t*)(address + 1) = hook - address - 5; + *(ptrdiff_t*)(address + 1) = (uintptr_t)hook - (uintptr_t)address - 5; if(type == PATCH_NOTHING) VirtualProtect((void*)(address + 1), 4, protect[0], &protect[1]); else From c953230237d3ac09b9e3fbfaf19b204f0cc568f1 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Sat, 28 Mar 2020 04:53:42 +0200 Subject: [PATCH 26/70] Set Xinput version to 9.1.0 + vibration set --- src/core/Pad.cpp | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/core/Pad.cpp b/src/core/Pad.cpp index 9a911aa4..87f45b9f 100644 --- a/src/core/Pad.cpp +++ b/src/core/Pad.cpp @@ -7,7 +7,7 @@ #include "common.h" #ifdef XINPUT #include -#pragma comment( lib, "Xinput.lib" ) +#pragma comment( lib, "Xinput9_1_0.lib" ) #endif #include "patcher.h" #include "Pad.h" @@ -590,6 +590,24 @@ void CPad::AffectFromXinput(uint32 pad) PCTempJoyState.RightStickX = (int32)(rx * 128.0f); PCTempJoyState.RightStickY = (int32)(ry * 128.0f); } + + XINPUT_VIBRATION VibrationState; + + memset(&VibrationState, 0, sizeof(XINPUT_VIBRATION)); + + uint16 iLeftMotor = (uint16)((float)ShakeFreq / 255.0f * (float)0xffff); + uint16 iRightMotor = (uint16)((float)ShakeFreq / 255.0f * (float)0xffff); + + if (ShakeDur < CTimer::GetTimeStepInMilliseconds()) + ShakeDur = 0; + else + ShakeDur -= CTimer::GetTimeStepInMilliseconds(); + if (ShakeDur == 0) ShakeFreq = 0; + + VibrationState.wLeftMotorSpeed = iLeftMotor; + VibrationState.wRightMotorSpeed = iRightMotor; + + XInputSetState(pad, &VibrationState); } } #endif @@ -617,6 +635,7 @@ void CPad::UpdatePads(void) if ( bUpdate ) { GetPad(0)->Update(0); + GetPad(1)->Update(0); } #if defined(MASTER) && !defined(XINPUT) From cd913404a3f2fd12a90c03ed62c25ea425a23814 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Sat, 28 Mar 2020 04:58:58 +0200 Subject: [PATCH 27/70] Fix Text n Pager --- src/text/Pager.h | 28 +-- src/text/Text.cpp | 542 +++++++++++++++++++++++----------------------- 2 files changed, 285 insertions(+), 285 deletions(-) diff --git a/src/text/Pager.h b/src/text/Pager.h index 727eeb24..1719e726 100644 --- a/src/text/Pager.h +++ b/src/text/Pager.h @@ -1,5 +1,5 @@ #pragma once - + struct PagerMessage { wchar *m_pText; uint16 m_nSpeedMs; @@ -9,20 +9,20 @@ struct PagerMessage { uint32 m_nTimeToChangePosition; int16 field_10; int32 m_nNumber[6]; -}; - -#define NUMPAGERMESSAGES 8 - -class CPager -{ +}; + +#define NUMPAGERMESSAGES 8 + +class CPager +{ int16 m_nNumDisplayLetters; - PagerMessage m_messages[NUMPAGERMESSAGES]; + PagerMessage m_messages[NUMPAGERMESSAGES]; public: - void Init(); - void Process(); - void Display(); + void Init(); + void Process(); + void Display(); void AddMessage(wchar*, uint16, uint16, uint16); - void AddMessageWithNumber(wchar *str, int32 n1, int32 n2, int32 n3, int32 n4, int32 n5, int32 n6, uint16 speed, uint16 priority, uint16 a11); - void ClearMessages(); - void RestartCurrentMessage(); + void AddMessageWithNumber(wchar *str, int32 n1, int32 n2, int32 n3, int32 n4, int32 n5, int32 n6, uint16 speed, uint16 priority, uint16 a11); + void ClearMessages(); + void RestartCurrentMessage(); }; \ No newline at end of file diff --git a/src/text/Text.cpp b/src/text/Text.cpp index 40717ed5..8bffa7e1 100644 --- a/src/text/Text.cpp +++ b/src/text/Text.cpp @@ -1,92 +1,92 @@ -#include "common.h" -#include "patcher.h" -#include "FileMgr.h" -#include "Frontend.h" -#include "Messages.h" -#include "Text.h" - -static wchar WideErrorString[25]; - -CText &TheText = *(CText*)0x941520; - -CText::CText(void) -{ - encoding = 'e'; - memset(WideErrorString, 0, sizeof(WideErrorString)); -} - -void -CText::Load(void) -{ - uint8 *filedata; - char filename[32], type[4]; - int length; - int offset, sectlen; - - Unload(); - filedata = new uint8[0x40000]; - - CFileMgr::SetDir("TEXT"); - switch(CMenuManager::m_PrefsLanguage){ - case LANGUAGE_AMERICAN: - sprintf(filename, "AMERICAN.GXT"); - break; - case LANGUAGE_FRENCH: - sprintf(filename, "FRENCH.GXT"); - break; - case LANGUAGE_GERMAN: - sprintf(filename, "GERMAN.GXT"); - break; - case LANGUAGE_ITALIAN: - sprintf(filename, "ITALIAN.GXT"); - break; - case LANGUAGE_SPANISH: - sprintf(filename, "SPANISH.GXT"); - break; - } - - length = CFileMgr::LoadFile(filename, filedata, 0x40000, "rb"); - CFileMgr::SetDir(""); - - offset = 0; - while(offset < length){ - type[0] = filedata[offset++]; - type[1] = filedata[offset++]; - type[2] = filedata[offset++]; - type[3] = filedata[offset++]; - sectlen = (int)filedata[offset+3]<<24 | (int)filedata[offset+2]<<16 | - (int)filedata[offset+1]<<8 | (int)filedata[offset+0]; - offset += 4; - if(sectlen != 0){ - if(strncmp(type, "TKEY", 4) == 0) - keyArray.Load(sectlen, filedata, &offset); - else if(strncmp(type, "TDAT", 4) == 0) - data.Load(sectlen, filedata, &offset); - else - offset += sectlen; - } - } - - keyArray.Update(data.chars); - - delete[] filedata; -} - -void -CText::Unload(void) -{ - CMessages::ClearAllMessagesDisplayedByGame(); - data.Unload(); - keyArray.Unload(); -} - -wchar* -CText::Get(const char *key) -{ - return keyArray.Search(key); -} - -wchar UpperCaseTable[128] = { +#include "common.h" +#include "patcher.h" +#include "FileMgr.h" +#include "Frontend.h" +#include "Messages.h" +#include "Text.h" + +static wchar WideErrorString[25]; + +CText &TheText = *(CText*)0x941520; + +CText::CText(void) +{ + encoding = 'e'; + memset(WideErrorString, 0, sizeof(WideErrorString)); +} + +void +CText::Load(void) +{ + uint8 *filedata; + char filename[32], type[4]; + int length; + int offset, sectlen; + + Unload(); + filedata = new uint8[0x40000]; + + CFileMgr::SetDir("TEXT"); + switch(CMenuManager::m_PrefsLanguage){ + case LANGUAGE_AMERICAN: + sprintf(filename, "AMERICAN.GXT"); + break; + case LANGUAGE_FRENCH: + sprintf(filename, "FRENCH.GXT"); + break; + case LANGUAGE_GERMAN: + sprintf(filename, "GERMAN.GXT"); + break; + case LANGUAGE_ITALIAN: + sprintf(filename, "ITALIAN.GXT"); + break; + case LANGUAGE_SPANISH: + sprintf(filename, "SPANISH.GXT"); + break; + } + + length = CFileMgr::LoadFile(filename, filedata, 0x40000, "rb"); + CFileMgr::SetDir(""); + + offset = 0; + while(offset < length){ + type[0] = filedata[offset++]; + type[1] = filedata[offset++]; + type[2] = filedata[offset++]; + type[3] = filedata[offset++]; + sectlen = (int)filedata[offset+3]<<24 | (int)filedata[offset+2]<<16 | + (int)filedata[offset+1]<<8 | (int)filedata[offset+0]; + offset += 4; + if(sectlen != 0){ + if(strncmp(type, "TKEY", 4) == 0) + keyArray.Load(sectlen, filedata, &offset); + else if(strncmp(type, "TDAT", 4) == 0) + data.Load(sectlen, filedata, &offset); + else + offset += sectlen; + } + } + + keyArray.Update(data.chars); + + delete[] filedata; +} + +void +CText::Unload(void) +{ + CMessages::ClearAllMessagesDisplayedByGame(); + data.Unload(); + keyArray.Unload(); +} + +wchar* +CText::Get(const char *key) +{ + return keyArray.Search(key); +} + +wchar UpperCaseTable[128] = { 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, @@ -98,10 +98,10 @@ wchar UpperCaseTable[128] = { 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, - 249, 250, 251, 252, 253, 254, 255 -}; - -wchar FrenchUpperCaseTable[128] = { + 249, 250, 251, 252, 253, 254, 255 +}; + +wchar FrenchUpperCaseTable[128] = { 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 65, 65, 65, 65, 132, 133, 69, 69, 69, 69, 73, 73, @@ -113,11 +113,11 @@ wchar FrenchUpperCaseTable[128] = { 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, - 253, 254, 255 -}; - -wchar -CText::GetUpperCase(wchar c) + 253, 254, 255 +}; + +wchar +CText::GetUpperCase(wchar c) { switch (encoding) { @@ -144,176 +144,176 @@ CText::GetUpperCase(wchar c) default: break; } - return c; -} - -void -CText::UpperCase(wchar *s) -{ - while(*s){ - *s = GetUpperCase(*s); - s++; - } -} - - -void -CKeyArray::Load(uint32 length, uint8 *data, int *offset) -{ - uint32 i; - uint8 *rawbytes; - - numEntries = length / sizeof(CKeyEntry); - entries = new CKeyEntry[numEntries]; - rawbytes = (uint8*)entries; - - for(i = 0; i < length; i++) - rawbytes[i] = data[(*offset)++]; -} - -void -CKeyArray::Unload(void) -{ - delete[] entries; - entries = nil; - numEntries = 0; -} - -void -CKeyArray::Update(wchar *chars) -{ - int i; - for(i = 0; i < numEntries; i++) - entries[i].value = (wchar*)((uint8*)chars + (uintptr)entries[i].value); -} - -CKeyEntry* -CKeyArray::BinarySearch(const char *key, CKeyEntry *entries, int16 low, int16 high) -{ - int mid; - int diff; - - if(low > high) - return nil; - - mid = (low + high)/2; - diff = strcmp(key, entries[mid].key); - if(diff == 0) - return &entries[mid]; - if(diff < 0) - return BinarySearch(key, entries, low, mid-1); - if(diff > 0) - return BinarySearch(key, entries, mid+1, high); - return nil; -} - -wchar* -CKeyArray::Search(const char *key) -{ - CKeyEntry *found; - char errstr[25]; - int i; - - found = BinarySearch(key, entries, 0, numEntries-1); - if(found) - return found->value; - sprintf(errstr, "%s missing", key); - for(i = 0; i < 25; i++) - WideErrorString[i] = errstr[i]; - return WideErrorString; -} - - -void -CData::Load(uint32 length, uint8 *data, int *offset) -{ - uint32 i; - uint8 *rawbytes; - - numChars = length / sizeof(wchar); - chars = new wchar[numChars]; - rawbytes = (uint8*)chars; - - for(i = 0; i < length; i++) - rawbytes[i] = data[(*offset)++]; -} - -void -CData::Unload(void) -{ - delete[] chars; - chars = nil; - numChars = 0; -} - -void -AsciiToUnicode(const char *src, wchar *dst) -{ - while((*dst++ = *src++) != '\0'); -} - -char* -UnicodeToAscii(wchar *src) -{ - static char aStr[256]; - int len; - for(len = 0; *src != '\0' && len < 256-1; len++, src++) - if(*src < 128) - aStr[len] = *src; - else - aStr[len] = '#'; - aStr[len] = '\0'; - return aStr; -} - -char* -UnicodeToAsciiForSaveLoad(wchar *src) -{ - static char aStr[256]; - int len; - for(len = 0; *src != '\0' && len < 256-1; len++, src++) - if(*src < 256) - aStr[len] = *src; - else - aStr[len] = '#'; - aStr[len] = '\0'; - return aStr; -} - -void -UnicodeStrcpy(wchar *dst, const wchar *src) -{ - while((*dst++ = *src++) != '\0'); -} - -int -UnicodeStrlen(const wchar *str) -{ - int len; - for(len = 0; *str != '\0'; len++, str++); - return len; -} - -void -TextCopy(wchar *dst, const wchar *src) -{ - while((*dst++ = *src++) != '\0'); -} - - -STARTPATCHES - InjectHook(0x52C3C0, &CText::Load, PATCH_JUMP); - InjectHook(0x52C580, &CText::Unload, PATCH_JUMP); - InjectHook(0x52C5A0, &CText::Get, PATCH_JUMP); - InjectHook(0x52C220, &CText::GetUpperCase, PATCH_JUMP); - InjectHook(0x52C2C0, &CText::UpperCase, PATCH_JUMP); - - InjectHook(0x52BE70, &CKeyArray::Load, PATCH_JUMP); - InjectHook(0x52BF60, &CKeyArray::Unload, PATCH_JUMP); - InjectHook(0x52BF80, &CKeyArray::Update, PATCH_JUMP); - InjectHook(0x52C060, &CKeyArray::BinarySearch, PATCH_JUMP); - InjectHook(0x52BFB0, &CKeyArray::Search, PATCH_JUMP); - - InjectHook(0x52C120, &CData::Load, PATCH_JUMP); - InjectHook(0x52C200, &CData::Unload, PATCH_JUMP); -ENDPATCHES + return c; +} + +void +CText::UpperCase(wchar *s) +{ + while(*s){ + *s = GetUpperCase(*s); + s++; + } +} + + +void +CKeyArray::Load(uint32 length, uint8 *data, int *offset) +{ + uint32 i; + uint8 *rawbytes; + + numEntries = length / sizeof(CKeyEntry); + entries = new CKeyEntry[numEntries]; + rawbytes = (uint8*)entries; + + for(i = 0; i < length; i++) + rawbytes[i] = data[(*offset)++]; +} + +void +CKeyArray::Unload(void) +{ + delete[] entries; + entries = nil; + numEntries = 0; +} + +void +CKeyArray::Update(wchar *chars) +{ + int i; + for(i = 0; i < numEntries; i++) + entries[i].value = (wchar*)((uint8*)chars + (uintptr)entries[i].value); +} + +CKeyEntry* +CKeyArray::BinarySearch(const char *key, CKeyEntry *entries, int16 low, int16 high) +{ + int mid; + int diff; + + if(low > high) + return nil; + + mid = (low + high)/2; + diff = strcmp(key, entries[mid].key); + if(diff == 0) + return &entries[mid]; + if(diff < 0) + return BinarySearch(key, entries, low, mid-1); + if(diff > 0) + return BinarySearch(key, entries, mid+1, high); + return nil; +} + +wchar* +CKeyArray::Search(const char *key) +{ + CKeyEntry *found; + char errstr[25]; + int i; + + found = BinarySearch(key, entries, 0, numEntries-1); + if(found) + return found->value; + sprintf(errstr, "%s missing", key); + for(i = 0; i < 25; i++) + WideErrorString[i] = errstr[i]; + return WideErrorString; +} + + +void +CData::Load(uint32 length, uint8 *data, int *offset) +{ + uint32 i; + uint8 *rawbytes; + + numChars = length / sizeof(wchar); + chars = new wchar[numChars]; + rawbytes = (uint8*)chars; + + for(i = 0; i < length; i++) + rawbytes[i] = data[(*offset)++]; +} + +void +CData::Unload(void) +{ + delete[] chars; + chars = nil; + numChars = 0; +} + +void +AsciiToUnicode(const char *src, wchar *dst) +{ + while((*dst++ = *src++) != '\0'); +} + +char* +UnicodeToAscii(wchar *src) +{ + static char aStr[256]; + int len; + for(len = 0; *src != '\0' && len < 256-1; len++, src++) + if(*src < 128) + aStr[len] = *src; + else + aStr[len] = '#'; + aStr[len] = '\0'; + return aStr; +} + +char* +UnicodeToAsciiForSaveLoad(wchar *src) +{ + static char aStr[256]; + int len; + for(len = 0; *src != '\0' && len < 256-1; len++, src++) + if(*src < 256) + aStr[len] = *src; + else + aStr[len] = '#'; + aStr[len] = '\0'; + return aStr; +} + +void +UnicodeStrcpy(wchar *dst, const wchar *src) +{ + while((*dst++ = *src++) != '\0'); +} + +int +UnicodeStrlen(const wchar *str) +{ + int len; + for(len = 0; *str != '\0'; len++, str++); + return len; +} + +void +TextCopy(wchar *dst, const wchar *src) +{ + while((*dst++ = *src++) != '\0'); +} + + +STARTPATCHES + InjectHook(0x52C3C0, &CText::Load, PATCH_JUMP); + InjectHook(0x52C580, &CText::Unload, PATCH_JUMP); + InjectHook(0x52C5A0, &CText::Get, PATCH_JUMP); + InjectHook(0x52C220, &CText::GetUpperCase, PATCH_JUMP); + InjectHook(0x52C2C0, &CText::UpperCase, PATCH_JUMP); + + InjectHook(0x52BE70, &CKeyArray::Load, PATCH_JUMP); + InjectHook(0x52BF60, &CKeyArray::Unload, PATCH_JUMP); + InjectHook(0x52BF80, &CKeyArray::Update, PATCH_JUMP); + InjectHook(0x52C060, &CKeyArray::BinarySearch, PATCH_JUMP); + InjectHook(0x52BFB0, &CKeyArray::Search, PATCH_JUMP); + + InjectHook(0x52C120, &CData::Load, PATCH_JUMP); + InjectHook(0x52C200, &CData::Unload, PATCH_JUMP); +ENDPATCHES From f5195ca185a248299e327e64a65a1a691bb8eddd Mon Sep 17 00:00:00 2001 From: aap Date: Sat, 28 Mar 2020 09:34:04 +0100 Subject: [PATCH 28/70] CCam fix --- src/core/Cam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index 12c72993..4ddde360 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -1601,7 +1601,7 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient entity = nil; } - if(CamTargetEntity->GetClump()){ + if(CamTargetEntity->m_rwObject){ // what's going on here? if(RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_PUMP) || RpAnimBlendClumpGetAssociation(CamTargetEntity->GetClump(), ANIM_WEAPON_THROW) || From 739e80614db70f726c47180173ba19688bec5167 Mon Sep 17 00:00:00 2001 From: aap Date: Sat, 28 Mar 2020 09:37:04 +0100 Subject: [PATCH 29/70] remove include from common.h --- src/animation/AnimBlendAssociation.cpp | 15 ++++++++++++--- src/animation/AnimBlendAssociation.h | 4 ---- src/animation/AnimBlendClumpData.cpp | 14 ++++++++++++-- src/animation/AnimBlendClumpData.h | 4 ---- src/core/Collision.cpp | 19 ++++++++++++++++--- src/core/Collision.h | 4 ---- src/core/Placeable.cpp | 2 ++ src/core/common.h | 2 -- src/entities/Building.cpp | 2 ++ src/entities/Entity.cpp | 2 ++ src/objects/DummyObject.cpp | 2 ++ src/objects/Object.cpp | 2 ++ src/objects/Projectile.cpp | 2 ++ src/peds/CivilianPed.cpp | 2 ++ src/peds/CopPed.cpp | 2 ++ src/peds/EmergencyPed.cpp | 2 ++ src/peds/Ped.cpp | 2 ++ src/peds/PlayerPed.cpp | 2 ++ src/vehicles/Automobile.cpp | 2 ++ src/vehicles/Boat.cpp | 2 ++ src/vehicles/Heli.cpp | 1 + src/vehicles/Plane.cpp | 1 + src/vehicles/Train.cpp | 2 ++ 23 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/animation/AnimBlendAssociation.cpp b/src/animation/AnimBlendAssociation.cpp index ec42191b..246322ba 100644 --- a/src/animation/AnimBlendAssociation.cpp +++ b/src/animation/AnimBlendAssociation.cpp @@ -203,6 +203,15 @@ CAnimBlendAssociation::UpdateBlend(float timeDelta) return true; } +#include + +class CAnimBlendAssociation_ : public CAnimBlendAssociation +{ +public: + CAnimBlendAssociation *ctor1(void) { return ::new (this) CAnimBlendAssociation(); } + CAnimBlendAssociation *ctor2(CAnimBlendAssociation &other) { return ::new (this) CAnimBlendAssociation(other); } + void dtor(void) { this->CAnimBlendAssociation::~CAnimBlendAssociation(); } +}; STARTPATCHES InjectHook(0x4016A0, &CAnimBlendAssociation::AllocateAnimBlendNodeArray, PATCH_JUMP); @@ -219,7 +228,7 @@ STARTPATCHES InjectHook(0x4031F0, &CAnimBlendAssociation::UpdateTime, PATCH_JUMP); InjectHook(0x4032B0, &CAnimBlendAssociation::UpdateBlend, PATCH_JUMP); - InjectHook(0x401460, &CAnimBlendAssociation::ctor1, PATCH_JUMP); - InjectHook(0x4014C0, &CAnimBlendAssociation::ctor2, PATCH_JUMP); - InjectHook(0x401520, &CAnimBlendAssociation::dtor, PATCH_JUMP); + InjectHook(0x401460, &CAnimBlendAssociation_::ctor1, PATCH_JUMP); + InjectHook(0x4014C0, &CAnimBlendAssociation_::ctor2, PATCH_JUMP); + InjectHook(0x401520, &CAnimBlendAssociation_::dtor, PATCH_JUMP); ENDPATCHES diff --git a/src/animation/AnimBlendAssociation.h b/src/animation/AnimBlendAssociation.h index aec28f56..d35db1db 100644 --- a/src/animation/AnimBlendAssociation.h +++ b/src/animation/AnimBlendAssociation.h @@ -85,9 +85,5 @@ public: static CAnimBlendAssociation *FromLink(CAnimBlendLink *l) { return (CAnimBlendAssociation*)((uint8*)l - offsetof(CAnimBlendAssociation, link)); } - - CAnimBlendAssociation *ctor1(void) { return ::new (this) CAnimBlendAssociation(); } - CAnimBlendAssociation *ctor2(CAnimBlendAssociation &other) { return ::new (this) CAnimBlendAssociation(other); } - void dtor(void) { this->CAnimBlendAssociation::~CAnimBlendAssociation(); } }; static_assert(sizeof(CAnimBlendAssociation) == 0x40, "CAnimBlendAssociation: error"); diff --git a/src/animation/AnimBlendClumpData.cpp b/src/animation/AnimBlendClumpData.cpp index 06625eb5..cc4281d6 100644 --- a/src/animation/AnimBlendClumpData.cpp +++ b/src/animation/AnimBlendClumpData.cpp @@ -36,9 +36,19 @@ CAnimBlendClumpData::ForAllFrames(void (*cb)(AnimBlendFrameData*, void*), void * cb(&frames[i], arg); } +#include + +class CAnimBlendClumpData_ : public CAnimBlendClumpData +{ +public: + CAnimBlendClumpData *ctor(void) { return ::new (this) CAnimBlendClumpData(); } + void dtor(void) { this->CAnimBlendClumpData::~CAnimBlendClumpData(); } +}; + + STARTPATCHES - InjectHook(0x401880, &CAnimBlendClumpData::ctor, PATCH_JUMP); - InjectHook(0x4018B0, &CAnimBlendClumpData::dtor, PATCH_JUMP); + InjectHook(0x401880, &CAnimBlendClumpData_::ctor, PATCH_JUMP); + InjectHook(0x4018B0, &CAnimBlendClumpData_::dtor, PATCH_JUMP); InjectHook(0x4018F0, &CAnimBlendClumpData::SetNumberOfFrames, PATCH_JUMP); InjectHook(0x401930, &CAnimBlendClumpData::ForAllFrames, PATCH_JUMP); ENDPATCHES diff --git a/src/animation/AnimBlendClumpData.h b/src/animation/AnimBlendClumpData.h index df2fbc56..1c8c391d 100644 --- a/src/animation/AnimBlendClumpData.h +++ b/src/animation/AnimBlendClumpData.h @@ -49,9 +49,5 @@ public: void SetNumberOfBones(int n) { SetNumberOfFrames(n); } #endif void ForAllFrames(void (*cb)(AnimBlendFrameData*, void*), void *arg); - - - CAnimBlendClumpData *ctor(void) { return ::new (this) CAnimBlendClumpData(); } - void dtor(void) { this->CAnimBlendClumpData::~CAnimBlendClumpData(); } }; static_assert(sizeof(CAnimBlendClumpData) == 0x14, "CAnimBlendClumpData: error"); diff --git a/src/core/Collision.cpp b/src/core/Collision.cpp index fc8428be..94ef769e 100644 --- a/src/core/Collision.cpp +++ b/src/core/Collision.cpp @@ -2061,6 +2061,19 @@ CColModel::operator=(const CColModel &other) return *this; } +#include +struct CColLine_ : public CColLine +{ + CColLine *ctor(CVector *p0, CVector *p1) { return ::new (this) CColLine(*p0, *p1); } +}; + +struct CColModel_ : public CColModel +{ + CColModel *ctor(void) { return ::new (this) CColModel(); } + void dtor(void) { this->CColModel::~CColModel(); } +}; + + STARTPATCHES InjectHook(0x4B9C30, (CMatrix& (*)(const CMatrix &src, CMatrix &dst))Invert, PATCH_JUMP); @@ -2099,15 +2112,15 @@ STARTPATCHES InjectHook(0x411E40, (void (CColSphere::*)(float, const CVector&, uint8, uint8))&CColSphere::Set, PATCH_JUMP); InjectHook(0x40B2A0, &CColBox::Set, PATCH_JUMP); - InjectHook(0x40B320, &CColLine::ctor, PATCH_JUMP); + InjectHook(0x40B320, &CColLine_::ctor, PATCH_JUMP); InjectHook(0x40B350, &CColLine::Set, PATCH_JUMP); InjectHook(0x411E70, &CColTriangle::Set, PATCH_JUMP); InjectHook(0x411EA0, &CColTrianglePlane::Set, PATCH_JUMP); InjectHook(0x412140, &CColTrianglePlane::GetNormal, PATCH_JUMP); - InjectHook(0x411680, &CColModel::ctor, PATCH_JUMP); - InjectHook(0x4116E0, &CColModel::dtor, PATCH_JUMP); + InjectHook(0x411680, &CColModel_::ctor, PATCH_JUMP); + InjectHook(0x4116E0, &CColModel_::dtor, PATCH_JUMP); InjectHook(0x411D80, &CColModel::RemoveCollisionVolumes, PATCH_JUMP); InjectHook(0x411CB0, &CColModel::CalculateTrianglePlanes, PATCH_JUMP); InjectHook(0x411D10, &CColModel::RemoveTrianglePlanes, PATCH_JUMP); diff --git a/src/core/Collision.h b/src/core/Collision.h index 9597a181..429fc17f 100644 --- a/src/core/Collision.h +++ b/src/core/Collision.h @@ -35,8 +35,6 @@ struct CColLine CColLine(void) { }; CColLine(const CVector &p0, const CVector &p1) { this->p0 = p0; this->p1 = p1; }; void Set(const CVector &p0, const CVector &p1); - - CColLine *ctor(CVector *p0, CVector *p1) { return ::new (this) CColLine(*p0, *p1); } }; struct CColTriangle @@ -106,8 +104,6 @@ struct CColModel void SetLinkPtr(CLink*); void GetTrianglePoint(CVector &v, int i) const; - CColModel *ctor(void) { return ::new (this) CColModel(); } - void dtor(void) { this->CColModel::~CColModel(); } CColModel& operator=(const CColModel& other); }; diff --git a/src/core/Placeable.cpp b/src/core/Placeable.cpp index d2cec82b..c882fc27 100644 --- a/src/core/Placeable.cpp +++ b/src/core/Placeable.cpp @@ -63,6 +63,8 @@ CPlaceable::IsWithinArea(float x1, float y1, float z1, float x2, float y2, float z1 <= GetPosition().z && GetPosition().z <= z2; } +#include + class CPlaceable_ : public CPlaceable { public: diff --git a/src/core/common.h b/src/core/common.h index 0cdff871..7b4ff4a0 100644 --- a/src/core/common.h +++ b/src/core/common.h @@ -10,8 +10,6 @@ #include #include #include -//#include -#include #ifdef WITHWINDOWS #include diff --git a/src/entities/Building.cpp b/src/entities/Building.cpp index 188cbfe7..7813c87f 100644 --- a/src/entities/Building.cpp +++ b/src/entities/Building.cpp @@ -21,6 +21,8 @@ CBuilding::ReplaceWithNewModel(int32 id) CStreaming::RequestModel(id, STREAMFLAGS_DONT_REMOVE); } +#include + class CBuilding_ : public CBuilding { public: diff --git a/src/entities/Entity.cpp b/src/entities/Entity.cpp index 04a93420..8bec1ac8 100644 --- a/src/entities/Entity.cpp +++ b/src/entities/Entity.cpp @@ -865,6 +865,8 @@ CEntity::ModifyMatrixForBannerInWind(void) UpdateRwFrame(); } +#include + class CEntity_ : public CEntity { public: diff --git a/src/objects/DummyObject.cpp b/src/objects/DummyObject.cpp index 9649cf7a..ba09ac3e 100644 --- a/src/objects/DummyObject.cpp +++ b/src/objects/DummyObject.cpp @@ -12,6 +12,8 @@ CDummyObject::CDummyObject(CObject *obj) m_level = obj->m_level; } +#include + class CDummyObject_ : public CDummyObject { public: diff --git a/src/objects/Object.cpp b/src/objects/Object.cpp index 357d67d7..809ba971 100644 --- a/src/objects/Object.cpp +++ b/src/objects/Object.cpp @@ -141,6 +141,8 @@ CObject::CanBeDeleted(void) } } +#include + class CObject_ : public CObject { public: diff --git a/src/objects/Projectile.cpp b/src/objects/Projectile.cpp index 0f6542e7..32bc6bdb 100644 --- a/src/objects/Projectile.cpp +++ b/src/objects/Projectile.cpp @@ -14,6 +14,8 @@ CProjectile::CProjectile(int32 model) : CObject() ObjectCreatedBy = MISSION_OBJECT; } +#include + class CProjectile_ : public CProjectile { public: diff --git a/src/peds/CivilianPed.cpp b/src/peds/CivilianPed.cpp index bb61e086..2e6166be 100644 --- a/src/peds/CivilianPed.cpp +++ b/src/peds/CivilianPed.cpp @@ -377,6 +377,8 @@ CCivilianPed::ProcessControl(void) Avoid(); } +#include + class CCivilianPed_ : public CCivilianPed { public: diff --git a/src/peds/CopPed.cpp b/src/peds/CopPed.cpp index dae866a4..94acac05 100644 --- a/src/peds/CopPed.cpp +++ b/src/peds/CopPed.cpp @@ -551,6 +551,8 @@ CCopPed::CopAI(void) } } +#include + class CCopPed_ : public CCopPed { public: diff --git a/src/peds/EmergencyPed.cpp b/src/peds/EmergencyPed.cpp index ee559f57..0d27a532 100644 --- a/src/peds/EmergencyPed.cpp +++ b/src/peds/EmergencyPed.cpp @@ -413,6 +413,8 @@ CEmergencyPed::MedicAI(void) } } +#include + class CEmergencyPed_ : public CEmergencyPed { public: diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index c8e8c4e4..05cac3a7 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -17455,6 +17455,8 @@ CPed::SetExitBoat(CVehicle *boat) CWaterLevel::FreeBoatWakeArray(); } +#include + class CPed_ : public CPed { public: diff --git a/src/peds/PlayerPed.cpp b/src/peds/PlayerPed.cpp index c6580d32..49d0183e 100644 --- a/src/peds/PlayerPed.cpp +++ b/src/peds/PlayerPed.cpp @@ -1414,6 +1414,8 @@ CPlayerPed::ProcessControl(void) } } +#include + class CPlayerPed_ : public CPlayerPed { public: diff --git a/src/vehicles/Automobile.cpp b/src/vehicles/Automobile.cpp index a05a1236..44ff6b6d 100644 --- a/src/vehicles/Automobile.cpp +++ b/src/vehicles/Automobile.cpp @@ -4483,6 +4483,8 @@ CAutomobile::SetAllTaxiLights(bool set) m_sAllTaxiLights = set; } +#include + class CAutomobile_ : public CAutomobile { public: diff --git a/src/vehicles/Boat.cpp b/src/vehicles/Boat.cpp index 7dbd7080..6d584017 100644 --- a/src/vehicles/Boat.cpp +++ b/src/vehicles/Boat.cpp @@ -299,6 +299,8 @@ CBoat::FillBoatList() } } +#include + class CBoat_ : public CBoat { public: diff --git a/src/vehicles/Heli.cpp b/src/vehicles/Heli.cpp index aab9dd0d..9fc50651 100644 --- a/src/vehicles/Heli.cpp +++ b/src/vehicles/Heli.cpp @@ -1034,6 +1034,7 @@ bool CHeli::HasCatalinaBeenShotDown(void) { return CatalinaHasBeenShotDown; } void CHeli::ActivateHeli(bool activate) { ScriptHeliOn = activate; } +#include class CHeli_ : public CHeli { diff --git a/src/vehicles/Plane.cpp b/src/vehicles/Plane.cpp index b4d80581..e44ff996 100644 --- a/src/vehicles/Plane.cpp +++ b/src/vehicles/Plane.cpp @@ -964,6 +964,7 @@ bool CPlane::HasCesnaLanded(void) { return CesnaMissionStatus == CESNA_STATUS_LA bool CPlane::HasCesnaBeenDestroyed(void) { return CesnaMissionStatus == CESNA_STATUS_DESTROYED; } bool CPlane::HasDropOffCesnaBeenShotDown(void) { return DropOffCesnaMissionStatus == CESNA_STATUS_DESTROYED; } +#include class CPlane_ : public CPlane { diff --git a/src/vehicles/Train.cpp b/src/vehicles/Train.cpp index 1c73ed05..6446e6d1 100644 --- a/src/vehicles/Train.cpp +++ b/src/vehicles/Train.cpp @@ -691,6 +691,8 @@ CTrain::UpdateTrains(void) } } +#include + class CTrain_ : public CTrain { public: From 268f92bfbe5330149be341855fa52f53ea501c62 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sat, 28 Mar 2020 15:24:13 +0300 Subject: [PATCH 30/70] garages part 1 --- src/audio/AudioManager.cpp | 12 +- src/audio/DMAudio.h | 2 +- src/control/Garages.cpp | 700 ++++++++++++++++++++++++++++++++++++- src/control/Garages.h | 84 ++++- src/control/Script.cpp | 8 +- src/core/Pad.h | 7 +- src/core/Stats.cpp | 1 + src/core/Stats.h | 1 + src/core/World.cpp | 1 + src/core/World.h | 1 + src/core/config.h | 2 + src/vehicles/Vehicle.cpp | 24 ++ src/vehicles/Vehicle.h | 2 + 13 files changed, 795 insertions(+), 50 deletions(-) diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 539c9e91..39c03ef6 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -3889,7 +3889,7 @@ cAudioManager::ProcessGarages() CalculateDistance(distCalculated, distSquared); \ m_sQueueSample.m_bVolume = ComputeVolume(60, 80.f, m_sQueueSample.m_fDistance); \ if(m_sQueueSample.m_bVolume) { \ - if(CGarages::Garages[i].m_eGarageType == GARAGE_CRUSHER) { \ + if(CGarages::aGarages[i].m_eGarageType == GARAGE_CRUSHER) { \ m_sQueueSample.m_nSampleIndex = SFX_COL_CAR_PANEL_2; \ m_sQueueSample.m_nFrequency = 6735; \ } else if(m_asAudioEntities[m_sQueueSample.m_nEntityIndex] \ @@ -3925,20 +3925,20 @@ cAudioManager::ProcessGarages() } for(uint32 i = 0; i < CGarages::NumGarages; ++i) { - if(CGarages::Garages[i].m_eGarageType == GARAGE_NONE) continue; - entity = CGarages::Garages[i].m_pDoor1; + if(CGarages::aGarages[i].m_eGarageType == GARAGE_NONE) continue; + entity = CGarages::aGarages[i].m_pDoor1; if(!entity) continue; m_sQueueSample.m_vecPos = entity->GetPosition(); distCalculated = false; distSquared = GetDistanceSquared(&m_sQueueSample.m_vecPos); if(distSquared < 6400.f) { - state = CGarages::Garages[i].m_eGarageState; + state = CGarages::aGarages[i].m_eGarageState; if(state == GS_OPENING || state == GS_CLOSING || state == GS_AFTERDROPOFF) { CalculateDistance(distCalculated, distSquared); m_sQueueSample.m_bVolume = ComputeVolume(90u, 80.f, m_sQueueSample.m_fDistance); if(m_sQueueSample.m_bVolume) { - if(CGarages::Garages[i].m_eGarageType == GARAGE_CRUSHER) { - if(CGarages::Garages[i].m_eGarageState == GS_AFTERDROPOFF) { + if(CGarages::aGarages[i].m_eGarageType == GARAGE_CRUSHER) { + if(CGarages::aGarages[i].m_eGarageState == GS_AFTERDROPOFF) { if(!(m_FrameCounter & 1)) { LOOP_HELPER continue; diff --git a/src/audio/DMAudio.h b/src/audio/DMAudio.h index 125263f0..41901c0d 100644 --- a/src/audio/DMAudio.h +++ b/src/audio/DMAudio.h @@ -65,7 +65,7 @@ enum eSound : int16 SOUND_GARAGE_NO_MONEY = 57, SOUND_GARAGE_BAD_VEHICLE = 58, SOUND_GARAGE_OPENING = 59, - SOUND_3C = 60, + SOUND_GARAGE_DENIED = 60, SOUND_GARAGE_BOMB1_SET = 61, SOUND_GARAGE_BOMB2_SET = 62, SOUND_GARAGE_BOMB3_SET = 63, diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 5ac15377..af443f8e 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -1,15 +1,55 @@ #include "common.h" #include "patcher.h" -#include "main.h" -#include "ModelIndices.h" #include "Garages.h" -#include "Timer.h" +#include "main.h" + +#include "General.h" #include "Font.h" +#include "Hud.h" #include "Messages.h" +#include "ModelIndices.h" +#include "Particle.h" #include "PlayerPed.h" +#include "Replay.h" +#include "Stats.h" #include "Text.h" +#include "Timer.h" +#include "Vehicle.h" #include "World.h" +#define CRUSHER_GARAGE_X1 (1135.5f) +#define CRUSHER_GARAGE_Y1 (7.0f) +#define CRUSHER_GARAGE_Z1 (-1.0f) +#define CRUSHER_GARAGE_X2 (1149.5f) +#define CRUSHER_GARAGE_Y2 (63.7f) +#define CRUSHER_GARAGE_Z2 (3.5f) + +#define ROTATED_DOOR_OPEN_SPEED (0.015f) +#define ROTATED_DOOR_CLOSE_SPEED (0.02f) +#define DEFAULT_DOOR_OPEN_SPEED (0.035f) +#define DEFAULT_DOOR_CLOSE_SPEED (0.04f) + +#define BOMB_PRICE 1000 +#define RESPRAY_PRICE 1000 + +#define DISTANCE_TO_CALL_OFF_CHASE 10.0f +#define DISTANCE_FOR_MRWHOOP_HACK 4.0f +#define DISTANCE_TO_ACTIVATE_GARAGE 8.0f +#define DISTANCE_TO_CLOSE_MISSION_GARAGE 30.0f +#define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE 25.0 + +#define TIME_TO_RESPRAY 2000 + +#define FREE_RESPRAY_HEALTH_THRESHOLD 970.0f +#define NUM_PARTICLES_IN_RESPRAY 200 + +#define KGS_OF_EXPLOSIVES_IN_BOMB 10 + +#define REWARD_FOR_FIRST_POLICE_CAR 5000 +#define REWARD_FOR_FIRST_BANK_VAN 5000 +#define MAX_POLICE_CARS_TO_COLLECT 10 +#define MAX_BANK_VANS_TO_COLLECT 10 + int32 &CGarages::BankVansCollected = *(int32 *)0x8F1B34; bool &CGarages::BombsAreFree = *(bool *)0x95CD7A; bool &CGarages::RespraysAreFree = *(bool *)0x95CD1D; @@ -26,13 +66,632 @@ uint32 &CGarages::NumGarages = *(uint32 *)0x8F29F4; bool &CGarages::PlayerInGarage = *(bool *)0x95CD83; int32 &CGarages::PoliceCarsCollected = *(int32 *)0x941444; uint32 &CGarages::GarageToBeTidied = *(uint32 *)0x623570; +CStoredCar(&CGarages::aCarsInSafeHouse1)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA210; +CStoredCar(&CGarages::aCarsInSafeHouse2)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA300; +CStoredCar(&CGarages::aCarsInSafeHouse3)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA3F0; +int32& CGarages::AudioEntity = *(int32*)0x5ECEA8; +CGarage(&CGarages::aGarages)[NUM_GARAGES] = *(CGarage(*)[NUM_GARAGES])*(uintptr*)0x72BCD0; +bool& CGarages::bCamShouldBeOutisde = *(bool*)0x95CDB2; -CGarage(&CGarages::Garages)[NUM_GARAGES] = *(CGarage(*)[NUM_GARAGES])*(uintptr*)0x72BCD0; +void CGarages::Init(void) +{ + CrushedCarId = -1; + NumGarages = 0; + MessageEndTime = 0; + MessageStartTime = 0; + PlayerInGarage = false; + BombsAreFree = false; + CarsCollected = 0; + BankVansCollected = 0; + PoliceCarsCollected = 0; + for (int i = 0; i < TOTAL_COLLECTCARS_GARAGES; i++) + CarTypesCollected[i] = 0; + LastTimeHelpMessage = 0; + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) + aCarsInSafeHouse1[i].Init(); + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) + aCarsInSafeHouse2[i].Init(); + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) + aCarsInSafeHouse3[i].Init(); + AudioEntity = DMAudio.CreateEntity(AUDIOTYPE_GARAGE, (void*)1); + if (AudioEntity >= 0) + DMAudio.SetEntityStatus(AudioEntity, 1); + AddOne( + CRUSHER_GARAGE_X1, CRUSHER_GARAGE_Y1, CRUSHER_GARAGE_Z1, + CRUSHER_GARAGE_X2, CRUSHER_GARAGE_Y2, CRUSHER_GARAGE_Z2, + GARAGE_CRUSHER, 0); +} + +#ifndef PS2 +void CGarages::Shutdown(void) +{ + NumGarages = 0; + if (AudioEntity < 0) + return; + DMAudio.DestroyEntity(AudioEntity); + AudioEntity = AEHANDLE_NONE; +} +#endif + +void CGarages::Update(void) +{ + static int GarageToBeTidied = 0; +#ifndef PS2 + if (CReplay::IsPlayingBack()) + return; +#endif + bCamShouldBeOutisde = false; + TheCamera.pToGarageWeAreIn = nil; + TheCamera.pToGarageWeAreInForHackAvoidFirstPerson = nil; + for (int i = 0; i < NUM_GARAGES; i++) { + if (aGarages[i].IsUsed()) + aGarages[i].Update(); + } + if ((CTimer::GetFrameCounter() & 0xF) != 0xC) + return; + if (++GarageToBeTidied >= 32) + GarageToBeTidied = 0; + if (!aGarages[GarageToBeTidied].IsUsed()) + return; + if (aGarages[GarageToBeTidied].IsClose()) + aGarages[GarageToBeTidied].TidyUpGarageClose(); + else + aGarages[GarageToBeTidied].TidyUpGarage(); +} + +int16 CGarages::AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z2, eGarageType type, int32 targetId) +{ + if (NumGarages >= NUM_GARAGES) { + assert(0); + return NumGarages++; + } + CGarage* pGarage = &aGarages[NumGarages]; + pGarage->m_fX1 = min(X1, X2); + pGarage->m_fX2 = max(X1, X2); + pGarage->m_fY1 = min(Y1, Y2); + pGarage->m_fY2 = max(Y1, Y2); + pGarage->m_fZ1 = min(Z1, Z2); + pGarage->m_fZ2 = max(Z1, Z2); + pGarage->m_pDoor1 = nil; + pGarage->m_pDoor2 = nil; + pGarage->m_fDoor1Z = Z1; + pGarage->m_fDoor2Z = Z1; + pGarage->m_eGarageType = type; + pGarage->field_24 = 0; + pGarage->m_bRotatedDoor = false; + pGarage->m_bCameraFollowsPlayer = false; + pGarage->RefreshDoorPointers(true); + if (pGarage->m_pDoor1) { + pGarage->m_fDoor1Z = pGarage->m_pDoor1->GetPosition().z; + pGarage->m_fDoor1X = pGarage->m_pDoor1->GetPosition().x; + pGarage->m_fDoor1Y = pGarage->m_pDoor1->GetPosition().y; + } + if (pGarage->m_pDoor2) { + pGarage->m_fDoor2Z = pGarage->m_pDoor2->GetPosition().z; + pGarage->m_fDoor2X = pGarage->m_pDoor2->GetPosition().x; + pGarage->m_fDoor2Y = pGarage->m_pDoor2->GetPosition().y; + } + pGarage->m_fDoorHeight = pGarage->m_pDoor1 ? FindDoorHeightForMI(pGarage->m_pDoor1->GetModelIndex()) : 4.0f; + pGarage->m_fDoorPos = 0.0f; + pGarage->m_eGarageState = GS_FULLYCLOSED; + pGarage->m_nTimeToStartAction = 0; + pGarage->field_2 = 0; + pGarage->m_nTargetModelIndex = targetId; + pGarage->field_96 = 0; + pGarage->m_bCollectedCarsState = 0; + pGarage->m_bDeactivated = false; + pGarage->m_bResprayHappened = false; + switch (type) { + case GARAGE_MISSION: + case GARAGE_COLLECTORSITEMS: + case GARAGE_COLLECTSPECIFICCARS: + case GARAGE_COLLECTCARS_1: + case GARAGE_COLLECTCARS_2: + case GARAGE_COLLECTCARS_3: + case GARAGE_FORCARTOCOMEOUTOF: + case GARAGE_60SECONDS: + case GARAGE_MISSION_KEEPCAR: + case GARAGE_FOR_SCRIPT_TO_OPEN: + case GARAGE_HIDEOUT_ONE: + case GARAGE_HIDEOUT_TWO: + case GARAGE_HIDEOUT_THREE: + case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: + case GARAGE_KEEPS_OPENING_FOR_SPECIFIC_CAR: + case GARAGE_MISSION_KEEPCAR_REMAINCLOSED: + pGarage->m_eGarageState = GS_FULLYCLOSED; + pGarage->m_fDoorPos = 0.0f; + break; + case GARAGE_BOMBSHOP1: + case GARAGE_BOMBSHOP2: + case GARAGE_BOMBSHOP3: + case GARAGE_RESPRAY: + pGarage->m_eGarageState = GS_OPENED; + pGarage->m_fDoorPos = pGarage->m_fDoorHeight; + break; + case GARAGE_CRUSHER: + pGarage->m_eGarageState = GS_OPENED; + pGarage->m_fDoorPos = HALFPI; + break; + default: + assert(false); + } + if (type == GARAGE_CRUSHER) + pGarage->UpdateCrusherAngle(); + else + pGarage->UpdateDoorsHeight(); + return NumGarages++; +} + +void CGarages::ChangeGarageType(int16 garage, eGarageType type, int32 mi) +{ + CGarage* pGarage = &aGarages[garage]; + pGarage->m_eGarageType = type; + pGarage->m_nTargetModelIndex = mi; + pGarage->m_eGarageState = GS_FULLYCLOSED; +} + +void CGarage::Update() +{ + if (m_eGarageType != GARAGE_CRUSHER) { + switch (m_eGarageState) { + case GS_FULLYCLOSED: + case GS_OPENED: + case GS_CLOSING: + case GS_OPENING: + case GS_OPENEDCONTAINSCAR: + case GS_CLOSEDCONTAINSCAR: + if (FindPlayerPed() && !m_bCameraFollowsPlayer) { + CVehicle* pVehicle = FindPlayerVehicle(); + if (IsEntityEntirelyInside3D(FindPlayerPed(), 0.25f)) { + TheCamera.pToGarageWeAreIn = this; + CGarages::bCamShouldBeOutisde = true; + } + if (pVehicle && IsEntityEntirelyOutside(pVehicle, 0.0f)) + TheCamera.pToGarageWeAreInForHackAvoidFirstPerson = this; + if (pVehicle->GetModelIndex() == MI_MRWHOOP) { + if (pVehicle->IsWithinArea( + m_fX1 - DISTANCE_FOR_MRWHOOP_HACK, + m_fX2 + DISTANCE_FOR_MRWHOOP_HACK, + m_fY1 - DISTANCE_FOR_MRWHOOP_HACK, + m_fY2 + DISTANCE_FOR_MRWHOOP_HACK)) { + TheCamera.pToGarageWeAreIn = this; + CGarages::bCamShouldBeOutisde = true; + } + } + } + } + } + if (m_bDeactivated && m_eGarageState == GS_FULLYCLOSED) + return; + switch (m_eGarageType) { + case GARAGE_RESPRAY: + switch (m_eGarageState) { + case GS_OPENED: + if (IsStaticPlayerCarEntirelyInside() && !IsAnyOtherCarTouchingGarage(FindPlayerVehicle())) { + if (IsCarSprayable()) { + if (CWorld::Players[CWorld::PlayerInFocus].m_nMoney >= RESPRAY_PRICE || CGarages::RespraysAreFree) { + m_eGarageState = GS_CLOSING; + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + } else { + CGarages::TriggerMessage("GA_3", -1, 4000, -1); // No more freebies. $1000 to respray! + m_eGarageState = GS_OPENEDCONTAINSCAR; + DMAudio.PlayFrontEndSound(SOUND_GARAGE_NO_MONEY, 1); + } + } else { + CGarages::TriggerMessage("GA_1", -1, 4000, -1); // Whoa! I don't touch nothing THAT hot! + m_eGarageState = GS_OPENEDCONTAINSCAR; + DMAudio.PlayFrontEndSound(SOUND_GARAGE_BAD_VEHICLE, 1); + } + } + if (FindPlayerVehicle()) { + if (CalcDistToGarageRectangleSquared(FindPlayerVehicle()->GetPosition().x, FindPlayerVehicle()->GetPosition().y) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) + CWorld::CallOffChaseForArea( + m_fX1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fY1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fX2 + DISTANCE_TO_CALL_OFF_CHASE, + m_fY2 + DISTANCE_TO_CALL_OFF_CHASE); + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_RESPRAY; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + CStats::CheckPointReachedSuccessfully(); + } + UpdateDoorsHeight(); +#ifdef FIX_BUGS + if (FindPlayerVehicle() && FindPlayerVehicle()->IsCar()) +#else + if (FindPlayerVehicle()) +#endif + ((CAutomobile*)(FindPlayerVehicle()))->m_fFireBlowUpTimer = 0.0f; + CWorld::CallOffChaseForArea( + m_fX1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fY1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fX2 + DISTANCE_TO_CALL_OFF_CHASE, + m_fY2 + DISTANCE_TO_CALL_OFF_CHASE); + break; + case GS_FULLYCLOSED: + if (CTimer::GetTimeInMilliseconds() > m_nTimeToStartAction) { + m_eGarageState = GS_OPENING; + DMAudio.PlayFrontEndSound(SOUND_GARAGE_OPENING, 1); + bool bTakeMoney = false; + if (FindPlayerPed()->m_pWanted->m_nWantedLevel != 0) + bTakeMoney = true; + FindPlayerPed()->m_pWanted->Reset(); + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; +#ifdef FIX_BUGS + bool bChangedColour = false; +#else + bool bChangedColour; +#endif + if (FindPlayerVehicle() && FindPlayerVehicle()->IsCar()) { + if (FindPlayerVehicle()->m_fHealth < FREE_RESPRAY_HEALTH_THRESHOLD) + bTakeMoney = true; + FindPlayerVehicle()->m_fHealth = 1000.0f; + ((CAutomobile*)(FindPlayerVehicle()))->m_fFireBlowUpTimer = 0.0f; + ((CAutomobile*)(FindPlayerVehicle()))->Fix(); + if (FindPlayerVehicle()->GetUp().z < 0.0f) { + FindPlayerVehicle()->GetUp() = -FindPlayerVehicle()->GetUp(); + FindPlayerVehicle()->GetRight() = -FindPlayerVehicle()->GetRight(); + } + bChangedColour = false; + if (!((CAutomobile*)(FindPlayerVehicle()))->bFixedColour) { + uint8 colour1, colour2; + uint16 attempt; + ((CVehicleModelInfo*)CModelInfo::GetModelInfo(FindPlayerVehicle()->GetModelIndex()))->ChooseVehicleColour(colour1, colour2); + for (attempt = 0; attempt < 10; attempt++) { + if (colour1 != FindPlayerVehicle()->m_currentColour1 || colour2 != FindPlayerVehicle()->m_currentColour2) + break; + ((CVehicleModelInfo*)CModelInfo::GetModelInfo(FindPlayerVehicle()->GetModelIndex()))->ChooseVehicleColour(colour1, colour2); + } + bChangedColour = (attempt < 10); + FindPlayerVehicle()->m_currentColour1 = colour1; + FindPlayerVehicle()->m_currentColour2 = colour2; + if (bChangedColour) { + for (int i = 0; i < NUM_PARTICLES_IN_RESPRAY; i++) { + CVector pos; +#ifdef FIX_BUGS + pos.x = CGeneral::GetRandomNumberInRange(m_fX1 + 0.5f, m_fX2 - 0.5f); + pos.y = CGeneral::GetRandomNumberInRange(m_fY1 + 0.5f, m_fY2 - 0.5f); + pos.z = CGeneral::GetRandomNumberInRange(m_fDoor1Z - 3.0f, m_fDoor1Z + 1.0f); +#else + // wtf is this + pos.x = m_fX1 + 0.5f + (uint8)(CGeneral::GetRandomNumber()) / 256.0f * (m_fX2 - m_fX1 - 1.0f); + pos.y = m_fY1 + 0.5f + (uint8)(CGeneral::GetRandomNumber()) / 256.0f * (m_fY2 - m_fY1 - 1.0f); + pos.z = m_fDoor1Z - 3.0f + (uint8)(CGeneral::GetRandomNumber()) / 256.0f * 4.0f; +#endif + CParticle::AddParticle(PARTICLE_GARAGEPAINT_SPRAY, pos, CVector(0.0f, 0.0f, 0.0f), nil, 0.0f, CVehicleModelInfo::ms_vehicleColourTable[colour1]); + } + } + } + CenterCarInGarage(FindPlayerVehicle()); + } + if (bTakeMoney) { + if (!CGarages::RespraysAreFree) + CWorld::Players[CWorld::PlayerInFocus].m_nMoney = max(0, CWorld::Players[CWorld::PlayerInFocus].m_nMoney - RESPRAY_PRICE); + CGarages::TriggerMessage("GA_2", -1, 4000, -1); // New engine and paint job. The cops won't recognize you! + } + else if (bChangedColour) { + if (CGeneral::GetRandomTrueFalse()) + CGarages::TriggerMessage("GA_15", -1, 4000, -1); // Hope you like the new color. + else + CGarages::TriggerMessage("GA_16", -1, 4000, -1); // Respray is complementary. + } + m_bResprayHappened = true; + } + CWorld::CallOffChaseForArea( + m_fX1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fY1 - DISTANCE_TO_CALL_OFF_CHASE, + m_fX2 + DISTANCE_TO_CALL_OFF_CHASE, + m_fY2 + DISTANCE_TO_CALL_OFF_CHASE); + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_OPENEDCONTAINSCAR: + if (IsPlayerOutsideGarage()) + m_eGarageState = GS_OPENED; + break; + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + } + break; + case GARAGE_BOMBSHOP1: + case GARAGE_BOMBSHOP2: + case GARAGE_BOMBSHOP3: + switch (m_eGarageState) { + case GS_OPENED: + if (IsStaticPlayerCarEntirelyInside() && !IsAnyOtherCarTouchingGarage(FindPlayerVehicle())) { +#ifdef FIX_BUGS // FindPlayerVehicle() can never be NULL here because IsStaticPlayerCarEntirelyInside() is true, and there is no IsCar() check + if (FindPlayerVehicle()->IsCar() && ((CAutomobile*)FindPlayerVehicle())->m_bombType) { +#else + if (!FindPlayerVehicle() || ((CAutomobile*)FindPlayerVehicle())->m_bombType) { +#endif + CGarages::TriggerMessage("GA_5", -1, 4000, -1); //"Your car is already fitted with a bomb" + m_eGarageState = GS_OPENEDCONTAINSCAR; + DMAudio.PlayFrontEndSound(SOUND_GARAGE_DENIED, 1); + break; + } + if (!CGarages::BombsAreFree && CWorld::Players[CWorld::PlayerInFocus].m_nMoney >= BOMB_PRICE) { + CGarages::TriggerMessage("GA_4", -1, 4000, -1); // "Car bombs are $1000 each" + m_eGarageState = GS_OPENEDCONTAINSCAR; + DMAudio.PlayFrontEndSound(SOUND_GARAGE_NO_MONEY, 1); + break; + } + m_eGarageState = GS_CLOSING; + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (CTimer::GetTimeInMilliseconds() > m_nTimeToStartAction) { + switch (m_eGarageType) { + case GARAGE_BOMBSHOP1: DMAudio.PlayFrontEndSound(SOUND_GARAGE_BOMB1_SET, 1); break; + case GARAGE_BOMBSHOP2: DMAudio.PlayFrontEndSound(SOUND_GARAGE_BOMB2_SET, 1); break; + case GARAGE_BOMBSHOP3: DMAudio.PlayFrontEndSound(SOUND_GARAGE_BOMB3_SET, 1); break; + } + m_eGarageState = GS_OPENING; + if (!CGarages::BombsAreFree) + CWorld::Players[CWorld::PlayerInFocus].m_nMoney = max(0, CWorld::Players[CWorld::PlayerInFocus].m_nMoney - BOMB_PRICE); + if (FindPlayerVehicle() && FindPlayerVehicle()->IsCar()) { + ((CAutomobile*)(FindPlayerVehicle()))->m_bombType = CGarages::GetBombTypeForGarageType(m_eGarageType); + ((CAutomobile*)(FindPlayerVehicle()))->m_pBombRigger = FindPlayerPed(); + if (m_eGarageType == GARAGE_BOMBSHOP3) + CGarages::GivePlayerDetonator(); + CStats::KgOfExplosivesUsed += KGS_OF_EXPLOSIVES_IN_BOMB; + } + switch (m_eGarageType) { + case GARAGE_BOMBSHOP1: + switch (CPad::GetPad(0)->Mode) { + case 0: + case 1: + case 2: + CHud::SetHelpMessage(TheText.Get("GA_6"), false); // Arm with ~h~~k~~PED_FIREWEAPON~ button~w~. Bomb will go off when engine is started. + break; + case 3: + CHud::SetHelpMessage(TheText.Get("GA_6B"), false); // Arm with ~h~~k~~PED_FIREWEAPON~ button~w~. Bomb will go off when engine is started. + break; + } + break; + case GARAGE_BOMBSHOP2: + switch (CPad::GetPad(0)->Mode) { + case 0: + case 1: + case 2: + CHud::SetHelpMessage(TheText.Get("GA_7"), false); // Park it, prime it by pressing the ~h~~k~~PED_FIREWEAPON~ button~w~ and LEG IT! + break; + case 3: + CHud::SetHelpMessage(TheText.Get("GA_7B"), false); // Park it, prime it by pressing the ~h~~k~~PED_FIREWEAPON~ button~w~ and LEG IT! + break; + } + break; + case GARAGE_BOMBSHOP3: + CHud::SetHelpMessage(TheText.Get("GA_8"), false); // Use the detonator to activate the bomb. + break; + } + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; + break; + } + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_OPENEDCONTAINSCAR: + if (IsPlayerOutsideGarage()) + m_eGarageState = GS_OPENED; + break; + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_MISSION: + switch (m_eGarageState) { + case GS_OPENED: + if (((CVector2D)FindPlayerCoors() - CVector2D(GetGarageCenterX(), GetGarageCenterY())).MagnitudeSqr() > SQR(DISTANCE_TO_CLOSE_MISSION_GARAGE)) { + if (!FindPlayerVehicle() && m_pTarget && IsEntityEntirelyInside3D(m_pTarget, 0.0f) && !IsAnyOtherCarTouchingGarage(m_pTarget)) { + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + m_eGarageState = GS_CLOSING; + m_bClosingWithoutTargetCar = false; + } + } + else if ((CTimer::GetFrameCounter() & 0x1F) == 0 && IsAnyOtherCarTouchingGarage(nil)) { + m_eGarageState = GS_CLOSING; + m_bClosingWithoutTargetCar = true; + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + if (m_bClosingWithoutTargetCar) + m_eGarageState = GS_FULLYCLOSED; + else { + if (m_pTarget) { + m_eGarageState = GS_CLOSEDCONTAINSCAR; + DestroyVehicleAndDriverAndPassengers(m_pTarget); + m_pTarget = nil; + } + else { + m_eGarageState = GS_FULLYCLOSED; + } + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; + } + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (FindPlayerVehicle() == m_pTarget && m_pTarget) { + if (CalcDistToGarageRectangleSquared( + FindPlayerVehicle()->GetPosition().x, + FindPlayerVehicle()->GetPosition().y) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) + m_eGarageState = GS_OPENING; + } + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_COLLECTSPECIFICCARS: + switch (m_eGarageState) { + case GS_OPENED: + if (FindPlayerVehicle() && m_nTargetModelIndex == FindPlayerVehicle()->GetModelIndex()) { + m_pTarget = FindPlayerVehicle(); + m_pTarget->RegisterReference((CEntity**)&m_pTarget); + } + if (!FindPlayerVehicle()) { + if (m_pTarget && IsEntityEntirelyInside3D(m_pTarget, 0.0f) && !IsAnyOtherCarTouchingGarage(m_pTarget)) { + if (IsEntityEntirelyOutside(FindPlayerPed(), 2.0f)) { + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + m_eGarageState = GS_CLOSING; + } + } + else if (Abs(FindPlayerCoors().x - GetGarageCenterX()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE || + Abs(FindPlayerCoors().y - GetGarageCenterY()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE) { + m_eGarageState = GS_CLOSING; + m_pTarget = nil; + } + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + if (m_pTarget) { + DestroyVehicleAndDriverAndPassengers(m_pTarget); + m_pTarget = nil; + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; + int16 reward; + switch (m_nTargetModelIndex) { + case MI_POLICE: + reward = REWARD_FOR_FIRST_POLICE_CAR * (MAX_POLICE_CARS_TO_COLLECT - CGarages::PoliceCarsCollected++) / MAX_POLICE_CARS_TO_COLLECT; + break; + case MI_SECURICA: + reward = REWARD_FOR_FIRST_BANK_VAN * (MAX_BANK_VANS_TO_COLLECT - CGarages::BankVansCollected++) / MAX_BANK_VANS_TO_COLLECT; + break; +#ifdef FIX_BUGS // not possible though + default: + reward = 0; + break; +#endif + } + if (reward > 0) { + CWorld::Players[CWorld::PlayerInFocus].m_nMoney += reward; + CGarages::TriggerMessage("GA_10", reward, 4000, -1); // Nice one. Here's your $~1~ + DMAudio.PlayFrontEndSound(SOUND_GARAGE_VEHICLE_ACCEPTED, 1); + } + else { + CGarages::TriggerMessage("GA_11", -1, 4000, -1); // We got these wheels already. It's worthless to us! + DMAudio.PlayFrontEndSound(SOUND_GARAGE_VEHICLE_DECLINED, 1); + } + } + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (FindPlayerVehicle() && m_nTargetModelIndex == FindPlayerVehicle()->GetModelIndex()) { + if (CalcDistToGarageRectangleSquared(FindPlayerVehicle()->GetPosition().x, FindPlayerVehicle()->GetPosition().y) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) + m_eGarageState = GS_OPENING; + } + break; + case GS_OPENING: + if (FindPlayerVehicle() && m_nTargetModelIndex == FindPlayerVehicle()->GetModelIndex()) { + m_pTarget = FindPlayerVehicle(); + m_pTarget->RegisterReference((CEntity**)&m_pTarget); + } + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + } + break; + case GARAGE_COLLECTORSITEMS: + case GARAGE_COLLECTCARS_1: + case GARAGE_COLLECTCARS_2: + case GARAGE_COLLECTCARS_3: + case GARAGE_FORCARTOCOMEOUTOF: + case GARAGE_60SECONDS: + case GARAGE_CRUSHER: + case GARAGE_MISSION_KEEPCAR: + case GARAGE_FOR_SCRIPT_TO_OPEN: + case GARAGE_HIDEOUT_ONE: + case GARAGE_HIDEOUT_TWO: + case GARAGE_HIDEOUT_THREE: + case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: + case GARAGE_KEEPS_OPENING_FOR_SPECIFIC_CAR: + case GARAGE_MISSION_KEEPCAR_REMAINCLOSED: + switch (m_eGarageState) { + case GS_FULLYCLOSED: + case GS_OPENING: + case GS_OPENED: + case GS_CLOSING: + case GS_OPENEDCONTAINSCAR: + case GS_CLOSEDCONTAINSCAR: + case GS_AFTERDROPOFF: + default: + } + break; + default: + break; + } + +} -WRAPPER void CGarages::Init(void) { EAXJMP(0x421C60); } -WRAPPER void CGarages::Update(void) { EAXJMP(0x421E40); } WRAPPER void CGarages::Load(uint8* buf, uint32 size) { EAXJMP(0x428940); } -WRAPPER void CGarages::Save(uint8* buf, uint32 *size) { EAXJMP(0x4284e0); } +WRAPPER void CGarages::Save(uint8* buf, uint32 *size) { EAXJMP(0x4284E0); } + +WRAPPER void CGarage::TidyUpGarageClose() { EAXJMP(0x427D90); } +WRAPPER void CGarage::TidyUpGarage() { EAXJMP(0x427C30); } +WRAPPER void CGarage::RefreshDoorPointers(bool) { EAXJMP(0x426980); } +WRAPPER void CGarage::UpdateCrusherAngle() { EAXJMP(0x4268A0); } +WRAPPER void CGarage::UpdateDoorsHeight() { EAXJMP(0x426730); } +WRAPPER float CGarages::FindDoorHeightForMI(int32) { EAXJMP(0x427C10); } bool CGarages::IsModelIndexADoor(uint32 id) @@ -81,7 +740,6 @@ WRAPPER void CGarages::TriggerMessage(const char *text, int16, uint16 time, int1 WRAPPER bool CGarages::IsPointWithinHideOutGarage(CVector&) { EAXJMP(0x428260); } WRAPPER bool CGarages::IsPointWithinAnyGarage(CVector&) { EAXJMP(0x428320); } WRAPPER void CGarages::PlayerArrestedOrDied() { EAXJMP(0x427F60); } -WRAPPER int16 CGarages::AddOne(float, float, float, float, float, float, uint8, uint32) { EAXJMP(0x421FA0); } WRAPPER void CGarages::SetTargetCarForMissonGarage(int16, CVehicle*) { EAXJMP(0x426BD0); } WRAPPER bool CGarages::HasCarBeenDroppedOffYet(int16) { EAXJMP(0x426C20); } WRAPPER void CGarages::DeActivateGarage(int16) { EAXJMP(0x426C40); } @@ -99,7 +757,6 @@ void CGarages::GivePlayerDetonator() } WRAPPER bool CGarages::HasThisCarBeenCollected(int16 garage, uint8 id) { EAXJMP(0x426D50); } -WRAPPER void CGarages::ChangeGarageType(int16 garage, eGarageType type, int32 mi) { EAXJMP(0x4222A0); } WRAPPER bool CGarages::HasResprayHappened(int16 garage) { EAXJMP(0x4274F0); } WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity* pCar) { EAXJMP(0x427570); } @@ -111,12 +768,12 @@ void CGarage::OpenThisGarage() bool CGarages::IsGarageOpen(int16 garage) { - return Garages[garage].IsOpen(); + return aGarages[garage].IsOpen(); } bool CGarages::IsGarageClosed(int16 garage) { - return Garages[garage].IsClosed(); + return aGarages[garage].IsClosed(); } void CGarage::CloseThisGarage() @@ -127,21 +784,21 @@ void CGarage::CloseThisGarage() void CGarages::SetGarageDoorToRotate(int16 garage) { - if (Garages[garage].m_bRotatedDoor) + if (aGarages[garage].m_bRotatedDoor) return; - Garages[garage].m_bRotatedDoor = true; - Garages[garage].m_fDoorHeight /= 2.0f; - Garages[garage].m_fDoorHeight -= 0.1f; + aGarages[garage].m_bRotatedDoor = true; + aGarages[garage].m_fDoorHeight /= 2.0f; + aGarages[garage].m_fDoorHeight -= 0.1f; } bool CGarages::HasImportExportGarageCollectedThisCar(int16 garage, int8 car) { - return CarTypesCollected[GetCarsCollectedIndexForGarageType(Garages[garage].m_eGarageType)] & (1 << car); + return CarTypesCollected[GetCarsCollectedIndexForGarageType(aGarages[garage].m_eGarageType)] & (1 << car); } void CGarages::SetLeaveCameraForThisGarage(int16 garage) { - Garages[garage].m_bCameraFollowsPlayer = true; + aGarages[garage].m_bCameraFollowsPlayer = true; } #if 0 @@ -187,4 +844,11 @@ void CGarages::PrintMessages() } } } -#endif \ No newline at end of file +#endif + +STARTPATCHES + InjectHook(0x421C60, CGarages::Init, PATCH_JUMP); +#ifndef PS2 + InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); +#endif +ENDPATCHES \ No newline at end of file diff --git a/src/control/Garages.h b/src/control/Garages.h index 5e106ade..89d51a05 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -1,6 +1,7 @@ #pragma once #include "Automobile.h" #include "audio_enums.h" +#include "Camera.h" #include "config.h" class CVehicle; @@ -63,34 +64,39 @@ class CStoredCar int8 m_nVariationA; int8 m_nVariationB; int8 m_nCarBombType; +public: + void Init() { m_nModelIndex = 0; } }; static_assert(sizeof(CStoredCar) == 0x28, "CStoredCar"); +#define SWITCH_GARAGE_DISTANCE_CLOSE 40.0f + class CGarage { public: eGarageType m_eGarageType; eGarageState m_eGarageState; char field_2; - char m_bClosingWithoutTargetCar; - char m_bDeactivated; - char m_bResprayHappened; - char field_6; - char field_7; + bool m_bClosingWithoutTargetCar; + bool m_bDeactivated; + bool m_bResprayHappened; int m_nTargetModelIndex; CEntity *m_pDoor1; CEntity *m_pDoor2; - char m_bDoor1PoolIndex; - char m_bDoor2PoolIndex; - char m_bIsDoor1Object; - char m_bIsDoor2Object; + uint8 m_bDoor1PoolIndex; + uint8 m_bDoor2PoolIndex; + bool m_bIsDoor1Object; + bool m_bIsDoor2Object; char field_24; - char m_bRotatedDoor; - char m_bCameraFollowsPlayer; - char field_27; - CVector m_vecInf; - CVector m_vecSup; + bool m_bRotatedDoor; + bool m_bCameraFollowsPlayer; + float m_fX1; + float m_fX2; + float m_fY1; + float m_fY2; + float m_fZ1; + float m_fZ2; float m_fDoorPos; float m_fDoorHeight; float m_fDoor1X; @@ -99,7 +105,7 @@ public: float m_fDoor2Y; float m_fDoor1Z; float m_fDoor2Z; - int m_nDoorOpenTime; + uint32 m_nTimeToStartAction; char m_bCollectedCarsState; char field_89; char field_90; @@ -112,6 +118,33 @@ public: void CloseThisGarage(); bool IsOpen() { return m_eGarageState == GS_OPENED || m_eGarageState == GS_OPENEDCONTAINSCAR; } bool IsClosed() { return m_eGarageState == GS_FULLYCLOSED; } + bool IsUsed() { return m_eGarageType != GARAGE_NONE; } + void Update(); + float GetGarageCenterX() { return (m_fX1 + m_fX2) / 2; } + float GetGarageCenterY() { return (m_fY1 + m_fY2) / 2; } + bool IsClose() + { +#ifdef FIX_BUGS + return Abs(TheCamera.GetPosition().x - GetGarageCenterX()) > SWITCH_GARAGE_DISTANCE_CLOSE || + Abs(TheCamera.GetPosition().y - GetGarageCenterY()) > SWITCH_GARAGE_DISTANCE_CLOSE; +#else + return Abs(TheCamera.GetPosition().x - m_fX1) > SWITCH_GARAGE_DISTANCE_CLOSE || + Abs(TheCamera.GetPosition().y - m_fY1) > SWITCH_GARAGE_DISTANCE_CLOSE; +#endif + } + void TidyUpGarageClose(); + void TidyUpGarage(); + void RefreshDoorPointers(bool); + void UpdateCrusherAngle(); + void UpdateDoorsHeight(); + bool IsEntityEntirelyInside3D(CEntity*, float); + bool IsEntityEntirelyOutside(CEntity*, float); + float CalcDistToGarageRectangleSquared(float, float); + bool IsAnyOtherCarTouchingGarage(CVehicle* pException); + bool IsStaticPlayerCarEntirelyInside(); + bool IsPlayerOutsideGarage(); + bool IsCarSprayable(); + void CenterCarInGarage(CVehicle*); }; static_assert(sizeof(CGarage) == 140, "CGarage"); @@ -135,9 +168,19 @@ public: static bool &PlayerInGarage; static int32 &PoliceCarsCollected; static uint32 &GarageToBeTidied; - static CGarage(&Garages)[NUM_GARAGES]; - + static CGarage(&aGarages)[NUM_GARAGES]; + static CStoredCar(&aCarsInSafeHouse1)[NUM_GARAGE_STORED_CARS]; + static CStoredCar(&aCarsInSafeHouse2)[NUM_GARAGE_STORED_CARS]; + static CStoredCar(&aCarsInSafeHouse3)[NUM_GARAGE_STORED_CARS]; + static int32 &AudioEntity; + static bool &bCamShouldBeOutisde; public: + static void Init(void); +#ifndef PS2 + static void Shutdown(void); +#endif + static int16 AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z2, eGarageType type, int32 targetId); + static bool IsModelIndexADoor(uint32 id); static void TriggerMessage(const char *text, int16, uint16 time, int16); static void PrintMessages(void); @@ -145,11 +188,10 @@ public: static bool IsPointWithinHideOutGarage(CVector&); static bool IsPointWithinAnyGarage(CVector&); static void PlayerArrestedOrDied(); - static void Init(void); + static void Update(void); static void Load(uint8 *buf, uint32 size); static void Save(uint8 *buf, uint32 *size); - static int16 AddOne(float, float, float, float, float, float, uint8, uint32); static void SetTargetCarForMissonGarage(int16, CVehicle*); static bool HasCarBeenDroppedOffYet(int16); static void ActivateGarage(int16); @@ -166,5 +208,9 @@ public: static void SetLeaveCameraForThisGarage(int16); static bool IsThisCarWithinGarageArea(int16, CEntity*); + static int GetBombTypeForGarageType(eGarageType type) { return type - GARAGE_BOMBSHOP1 + 1; } static int GetCarsCollectedIndexForGarageType(eGarageType type) { return type - GARAGE_COLLECTCARS_1; } + +private: + static float FindDoorHeightForMI(int32); }; diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 4aeacf3f..42b75554 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -4629,7 +4629,7 @@ int8 CRunningScript::ProcessCommands500To599(int32 command) infZ = *(float*)&ScriptParams[5]; supZ = *(float*)&ScriptParams[2]; } - ScriptParams[0] = CGarages::AddOne(infX, infY, infZ, supX, supY, supZ, ScriptParams[6], 0); + ScriptParams[0] = CGarages::AddOne(infX, infY, infZ, supX, supY, supZ, (eGarageType)ScriptParams[6], 0); StoreParameters(&m_nIp, 1); return 0; } @@ -4654,7 +4654,7 @@ int8 CRunningScript::ProcessCommands500To599(int32 command) infZ = *(float*)&ScriptParams[5]; supZ = *(float*)&ScriptParams[2]; } - ScriptParams[0] = CGarages::AddOne(infX, infY, infZ, supX, supY, supZ, ScriptParams[6], ScriptParams[7]); + ScriptParams[0] = CGarages::AddOne(infX, infY, infZ, supX, supY, supZ, (eGarageType)ScriptParams[6], ScriptParams[7]); StoreParameters(&m_nIp, 1); return 0; } @@ -7130,13 +7130,13 @@ int8 CRunningScript::ProcessCommands800To899(int32 command) case COMMAND_OPEN_GARAGE: { CollectParameters(&m_nIp, 1); - CGarages::Garages[ScriptParams[0]].OpenThisGarage(); + CGarages::aGarages[ScriptParams[0]].OpenThisGarage(); return 0; } case COMMAND_CLOSE_GARAGE: { CollectParameters(&m_nIp, 1); - CGarages::Garages[ScriptParams[0]].CloseThisGarage(); + CGarages::aGarages[ScriptParams[0]].CloseThisGarage(); return 0; } case COMMAND_WARP_CHAR_FROM_CAR_TO_COORD: diff --git a/src/core/Pad.h b/src/core/Pad.h index 09691128..a231900e 100644 --- a/src/core/Pad.h +++ b/src/core/Pad.h @@ -4,7 +4,7 @@ enum { PLAYERCONTROL_ENABLED = 0, PLAYERCONTROL_DISABLED_1 = 1, PLAYERCONTROL_DISABLED_2 = 2, - PLAYERCONTROL_DISABLED_4 = 4, + PLAYERCONTROL_GARAGE = 4, PLAYERCONTROL_DISABLED_8 = 8, PLAYERCONTROL_DISABLED_10 = 16, PLAYERCONTROL_DISABLED_20 = 32, // used on CPlayerInfo::MakePlayerSafe @@ -423,7 +423,10 @@ public: bool GetRightShoulder1(void) { return !!NewState.RightShoulder1; } bool GetRightShoulder2(void) { return !!NewState.RightShoulder2; } - bool ArePlayerControlsDisabled(void) { return DisablePlayerControls != PLAYERCONTROL_ENABLED; } + bool ArePlayerControlsDisabled(void) { return DisablePlayerControls != PLAYERCONTROL_ENABLED; } + void SetDisablePlayerControls(uint8 who) { DisablePlayerControls |= who; } + void SetEnablePlayerControls(uint8 who) { DisablePlayerControls &= ~who; } + bool IsPlayerControlsDisabledBy(uint8 who) { return DisablePlayerControls & who; } }; VALIDATE_SIZE(CPad, 0xFC); diff --git a/src/core/Stats.cpp b/src/core/Stats.cpp index 93eeb759..2a3f06b3 100644 --- a/src/core/Stats.cpp +++ b/src/core/Stats.cpp @@ -48,6 +48,7 @@ int32& CStats::LongestFlightInDodo = *(int32*)0x8F5FE4; int32& CStats::TimeTakenDefuseMission = *(int32*)0x880E24; int32& CStats::TotalNumberKillFrenzies = *(int32*)0x8E2884; int32& CStats::TotalNumberMissions = *(int32*)0x8E2820; +int32& CStats::KgOfExplosivesUsed = *(int32*)0x8F2510; int32(&CStats::FastestTimes)[CStats::TOTAL_FASTEST_TIMES] = *(int32(*)[CStats::TOTAL_FASTEST_TIMES])*(uintptr*)0x6E9128; int32(&CStats::HighestScores)[CStats::TOTAL_HIGHEST_SCORES] = *(int32(*)[CStats::TOTAL_HIGHEST_SCORES]) * (uintptr*)0x8622B0; diff --git a/src/core/Stats.h b/src/core/Stats.h index 0a750d5e..f6ff8187 100644 --- a/src/core/Stats.h +++ b/src/core/Stats.h @@ -53,6 +53,7 @@ public: static int32 &TotalNumberMissions; static int32(&FastestTimes)[TOTAL_FASTEST_TIMES]; static int32(&HighestScores)[TOTAL_HIGHEST_SCORES]; + static int32 &KgOfExplosivesUsed; public: static void RegisterFastestTime(int32, int32); diff --git a/src/core/World.cpp b/src/core/World.cpp index cbceb292..1832ce72 100644 --- a/src/core/World.cpp +++ b/src/core/World.cpp @@ -52,6 +52,7 @@ WRAPPER void CWorld::FindObjectsOfTypeInRangeSectorList(uint32, CPtrList&, CVect WRAPPER void CWorld::FindMissionEntitiesIntersectingCube(const CVector&, const CVector&, int16*, int16, CEntity**, bool, bool, bool) { EAXJMP(0x4B3680); } WRAPPER void CWorld::ClearCarsFromArea(float, float, float, float, float, float) { EAXJMP(0x4B50E0); } WRAPPER void CWorld::ClearPedsFromArea(float, float, float, float, float, float) { EAXJMP(0x4B52B0); } +WRAPPER void CWorld::CallOffChaseForArea(float, float, float, float) { EAXJMP(0x4B5530); } void CWorld::Initialise() diff --git a/src/core/World.h b/src/core/World.h index 1ad65ac4..75d17a71 100644 --- a/src/core/World.h +++ b/src/core/World.h @@ -117,6 +117,7 @@ public: static void FindMissionEntitiesIntersectingCube(const CVector&, const CVector&, int16*, int16, CEntity**, bool, bool, bool); static void ClearCarsFromArea(float, float, float, float, float, float); static void ClearPedsFromArea(float, float, float, float, float, float); + static void CallOffChaseForArea(float, float, float, float); static float GetSectorX(float f) { return ((f - WORLD_MIN_X)/SECTOR_SIZE_X); } static float GetSectorY(float f) { return ((f - WORLD_MIN_Y)/SECTOR_SIZE_Y); } diff --git a/src/core/config.h b/src/core/config.h index 9235e744..54a4c25f 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -114,6 +114,8 @@ enum Config { NUM_AUDIO_REFLECTIONS = 5, NUM_SCRIPT_MAX_ENTITIES = 40, + + NUM_GARAGE_STORED_CARS = 6 }; // We'll use this once we're ready to become independent of the game diff --git a/src/vehicles/Vehicle.cpp b/src/vehicles/Vehicle.cpp index 90848d6c..63c9519f 100644 --- a/src/vehicles/Vehicle.cpp +++ b/src/vehicles/Vehicle.cpp @@ -16,6 +16,7 @@ #include "Renderer.h" #include "DMAudio.h" #include "Radar.h" +#include "Darkel.h" bool &CVehicle::bWheelsOnlyCheat = *(bool *)0x95CD78; bool &CVehicle::bAllDodosCheat = *(bool *)0x95CD75; @@ -763,6 +764,29 @@ CVehicle::IsSphereTouchingVehicle(float sx, float sy, float sz, float radius) return true; } +void +DestroyVehicleAndDriverAndPassengers(CVehicle* pVehicle) +{ + if (pVehicle->pDriver) { +#ifndef FIX_BUGS + // this just isn't fair + CDarkel::RegisterKillByPlayer(pVehicle->pDriver, WEAPONTYPE_UNIDENTIFIED); +#endif + pVehicle->pDriver->FlagToDestroyWhenNextProcessed(); + } + for (int i = 0; i < pVehicle->m_nNumMaxPassengers; i++) { + if (pVehicle->pPassengers[i]) { +#ifndef FIX_BUGS + // this just isn't fair + CDarkel::RegisterKillByPlayer(pVehicle->pPassengers[i], WEAPONTYPE_UNIDENTIFIED); +#endif + pVehicle->pPassengers[i]->FlagToDestroyWhenNextProcessed(); + } + } + CWorld::Remove(pVehicle); + delete pVehicle; +} + class CVehicle_ : public CVehicle { diff --git a/src/vehicles/Vehicle.h b/src/vehicles/Vehicle.h index bd8df694..8c0825cf 100644 --- a/src/vehicles/Vehicle.h +++ b/src/vehicles/Vehicle.h @@ -300,3 +300,5 @@ public: }; static_assert(sizeof(cVehicleParams) == 0x18, "cVehicleParams: error"); + +void DestroyVehicleAndDriverAndPassengers(CVehicle* pVehicle); From 3366cd0ff80c7770ef280858992b7c707f0890d2 Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Sat, 28 Mar 2020 17:02:44 +0300 Subject: [PATCH 31/70] WaterCannon done, resource ico --- src/audio/AudioManager.cpp | 4 +- src/math/Vector.h | 8 + src/render/WaterCannon.cpp | 320 ++++++++++++++++++++++++++++++++++++- src/render/WaterCannon.h | 39 +++-- src/skel/win/resource.h | 1 + src/skel/win/win.rc | 12 +- 6 files changed, 363 insertions(+), 21 deletions(-) diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 539c9e91..930c03a6 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -7533,8 +7533,8 @@ cAudioManager::ProcessVehicleSkidding(cVehicleParams *params) void cAudioManager::ProcessWaterCannon(int32) { for(int32 i = 0; i < NUM_WATERCANNONS; i++) { - if(aCannons[i].m_nId) { - m_sQueueSample.m_vecPos = aCannons[0].m_avecPos[aCannons[i].m_wIndex]; + if(CWaterCannons::aCannons[i].m_nId) { + m_sQueueSample.m_vecPos = CWaterCannons::aCannons[0].m_avecPos[CWaterCannons::aCannons[i].m_nCur]; float distSquared = GetDistanceSquared(&m_sQueueSample.m_vecPos); if(distSquared < 900.f) { m_sQueueSample.m_fDistance = Sqrt(distSquared); diff --git a/src/math/Vector.h b/src/math/Vector.h index cd436123..6f544ada 100644 --- a/src/math/Vector.h +++ b/src/math/Vector.h @@ -38,6 +38,14 @@ public: }else x = 1.0f; } + + void Normalise(float norm) { + float sq = MagnitudeSqr(); + float invsqrt = RecipSqrt(norm, sq); + x *= invsqrt; + y *= invsqrt; + z *= invsqrt; + } const CVector &operator+=(CVector const &right) { x += right.x; diff --git a/src/render/WaterCannon.cpp b/src/render/WaterCannon.cpp index 7a9aa4d9..23c2293c 100644 --- a/src/render/WaterCannon.cpp +++ b/src/render/WaterCannon.cpp @@ -1,10 +1,320 @@ #include "common.h" #include "patcher.h" #include "WaterCannon.h" +#include "Vector.h" +#include "General.h" +#include "main.h" +#include "Timer.h" +#include "Pools.h" +#include "Ped.h" +#include "AnimManager.h" +#include "Fire.h" +#include "WaterLevel.h" +#include "Camera.h" -CWaterCannon (&aCannons)[NUM_WATERCANNONS] = *(CWaterCannon(*)[NUM_WATERCANNONS])*(uintptr*)0x8F2CA8; +#define WATERCANNONVERTS 4 +#define WATERCANNONINDEXES 12 -WRAPPER void CWaterCannons::Update(void) { EAXJMP(0x522510); } -WRAPPER void CWaterCannons::UpdateOne(uint32 id, CVector *pos, CVector *dir) { EAXJMP(0x522470); } -WRAPPER void CWaterCannons::Render(void) { EAXJMP(0x522550); } -WRAPPER void CWaterCannons::Init(void) { EAXJMP(0x522440); } +RwIm3DVertex WaterCannonVertices[WATERCANNONVERTS]; +RwImVertexIndex WaterCannonIndexList[WATERCANNONINDEXES]; + +CWaterCannon CWaterCannons::aCannons[NUM_WATERCANNONS]; + +void CWaterCannon::Init(void) +{ + m_nId = 0; + m_nCur = 0; + m_nTimeCreated = CTimer::GetTimeInMilliseconds(); + + for ( int32 i = 0; i < NUM_SEGMENTPOINTS; i++ ) + m_abUsed[i] = false; + + WaterCannonVertices[0].u = 0.0f; + WaterCannonVertices[0].v = 0.0f; + + WaterCannonVertices[1].u = 1.0f; + WaterCannonVertices[1].v = 0.0f; + + WaterCannonVertices[2].u = 0.0f; + WaterCannonVertices[2].v = 0.0f; + + WaterCannonVertices[3].u = 1.0f; + WaterCannonVertices[3].v = 0.0f; + + WaterCannonIndexList[0] = 0; + WaterCannonIndexList[1] = 1; + WaterCannonIndexList[2] = 2; + + WaterCannonIndexList[3] = 1; + WaterCannonIndexList[4] = 3; + WaterCannonIndexList[5] = 2; + + WaterCannonIndexList[6] = 0; + WaterCannonIndexList[7] = 2; + WaterCannonIndexList[8] = 1; + + WaterCannonIndexList[9] = 1; + WaterCannonIndexList[10] = 2; + WaterCannonIndexList[11] = 3; +} + +void CWaterCannon::Update_OncePerFrame(int16 index) +{ + ASSERT(index < NUM_WATERCANNONS); + + if (CTimer::GetTimeInMilliseconds() > m_nTimeCreated + WATERCANNON_LIFETIME ) + { + m_nCur = (m_nCur + 1) % -NUM_SEGMENTPOINTS; + m_abUsed[m_nCur] = false; + } + + for ( int32 i = 0; i < NUM_SEGMENTPOINTS; i++ ) + { + if ( m_abUsed[i] ) + { + m_avecVelocity[i].z += -WATERCANNON_GRAVITY * CTimer::GetTimeStep(); + m_avecPos[i] += m_avecVelocity[i] * CTimer::GetTimeStep(); + } + } + + int32 extinguishingPoint = CGeneral::GetRandomNumber() & (NUM_SEGMENTPOINTS - 1); + if ( m_abUsed[extinguishingPoint] ) + gFireManager.ExtinguishPoint(m_avecPos[extinguishingPoint], 3.0f); + + if ( ((index + CTimer::GetFrameCounter()) & 3) == 0 ) + PushPeds(); + + // free if unused + + int32 i = 0; + while ( 1 ) + { + if ( m_abUsed[i] ) + break; + + if ( ++i >= NUM_SEGMENTPOINTS ) + { + m_nId = 0; + return; + } + } +} + +void CWaterCannon::Update_NewInput(CVector *pos, CVector *dir) +{ + ASSERT(pos != NULL); + ASSERT(dir != NULL); + + m_avecPos[m_nCur] = *pos; + m_avecVelocity[m_nCur] = *dir; + m_abUsed[m_nCur] = true; +} + +void CWaterCannon::Render(void) +{ + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATEFOGENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)gpWaterRaster); + + float v = float(CGeneral::GetRandomNumber() & 255) / 256; + + WaterCannonVertices[0].v = v; + WaterCannonVertices[1].v = v; + WaterCannonVertices[2].v = v; + WaterCannonVertices[3].v = v; + + int16 pointA = m_nCur % -NUM_SEGMENTPOINTS; + + int16 pointB = pointA - 1; + if ( (pointA - 1) < 0 ) + pointB += NUM_SEGMENTPOINTS; + + bool bInit = false; + CVector norm; + + for ( int32 i = 0; i < NUM_SEGMENTPOINTS - 1; i++ ) + { + if ( m_abUsed[pointA] && m_abUsed[pointB] ) + { + if ( !bInit ) + { + CVector cp = CrossProduct(m_avecPos[pointB] - m_avecPos[pointA], TheCamera.GetForward()); + cp.Normalise(0.05f); + norm = cp; + bInit = true; + } + + float dist = float(i*i*i) / 300.0f + 1.0f; + float brightness = float(i) / NUM_SEGMENTPOINTS; + + int32 color = (int32)((1.0f - brightness*brightness) * 255.0f); + CVector offset = dist * norm; + + RwIm3DVertexSetRGBA(&WaterCannonVertices[0], color, color, color, color); + RwIm3DVertexSetPos (&WaterCannonVertices[0], m_avecPos[pointA].x - offset.x, m_avecPos[pointA].y - offset.y, m_avecPos[pointA].z - offset.z); + + RwIm3DVertexSetRGBA(&WaterCannonVertices[1], color, color, color, color); + RwIm3DVertexSetPos (&WaterCannonVertices[1], m_avecPos[pointA].x + offset.x, m_avecPos[pointA].y + offset.y, m_avecPos[pointA].z + offset.z); + + RwIm3DVertexSetRGBA(&WaterCannonVertices[2], color, color, color, color); + RwIm3DVertexSetPos (&WaterCannonVertices[2], m_avecPos[pointB].x - offset.x, m_avecPos[pointB].y - offset.y, m_avecPos[pointB].z - offset.z); + + RwIm3DVertexSetRGBA(&WaterCannonVertices[3], color, color, color, color); + RwIm3DVertexSetPos (&WaterCannonVertices[3], m_avecPos[pointB].x + offset.x, m_avecPos[pointB].y + offset.y, m_avecPos[pointB].z + offset.z); + + LittleTest(); + + if ( RwIm3DTransform(WaterCannonVertices, WATERCANNONVERTS, NULL, rwIM3D_VERTEXUV) ) + { + RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, WaterCannonIndexList, WATERCANNONINDEXES); + RwIm3DEnd(); + } + } + + pointA = pointB--; + if ( pointB < 0 ) + pointB += NUM_SEGMENTPOINTS; + } + + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATEFOGENABLE, (void *)FALSE); +} + +void CWaterCannon::PushPeds(void) +{ + float minx = 10000.0f; + float maxx = -10000.0f; + float miny = 10000.0f; + float maxy = -10000.0f; + float minz = 10000.0f; + float maxz = -10000.0f; + + for ( int32 i = 0; i < NUM_SEGMENTPOINTS; i++ ) + { + if ( m_abUsed[i] ) + { + minx = min(minx, m_avecPos[i].x); + maxx = max(maxx, m_avecPos[i].x); + + miny = min(miny, m_avecPos[i].y); + maxy = max(maxy, m_avecPos[i].y); + + minz = min(minz, m_avecPos[i].z); + maxz = max(maxz, m_avecPos[i].z); + } + } + + for ( int32 i = CPools::GetPedPool()->GetSize() - 1; i >= 0; i--) + { + CPed *ped = CPools::GetPedPool()->GetSlot(i); + if ( ped ) + { + if ( ped->GetPosition().x > minx && ped->GetPosition().x < maxx + && ped->GetPosition().y > miny && ped->GetPosition().y < maxy + && ped->GetPosition().z > minz && ped->GetPosition().z < maxz ) + { + for ( int32 j = 0; j < NUM_SEGMENTPOINTS; j++ ) + { + if ( m_abUsed[j] ) + { + CVector dist = m_avecPos[j] - ped->GetPosition(); + + if ( dist.MagnitudeSqr() < 5.0f ) + { + int32 localDir = ped->GetLocalDirection(CVector2D(1.0f, 0.0f)); + + ped->bIsStanding = false; + + ped->ApplyMoveForce(0.0f, 0.0f, 2.0f * CTimer::GetTimeStep()); + + ped->m_vecMoveSpeed.x = (0.6f * m_avecVelocity[j].x + ped->m_vecMoveSpeed.x) * 0.5f; + ped->m_vecMoveSpeed.y = (0.6f * m_avecVelocity[j].y + ped->m_vecMoveSpeed.y) * 0.5f; + + ped->SetFall(2000, AnimationId(ANIM_KO_SKID_FRONT + localDir), 0); + + CFire *fire = ped->m_pFire; + if ( fire ) + fire->Extinguish(); + + j = NUM_SEGMENTPOINTS; + } + } + } + } + } + } +} + +void CWaterCannons::Init(void) +{ + for ( int32 i = 0; i < NUM_WATERCANNONS; i++ ) + aCannons[i].Init(); +} + +void CWaterCannons::UpdateOne(uint32 id, CVector *pos, CVector *dir) +{ + ASSERT(pos != NULL); + ASSERT(dir != NULL); + + // find the one by id + { + int32 n = 0; + while ( n < NUM_WATERCANNONS && id != aCannons[n].m_nId ) + n++; + + if ( n < NUM_WATERCANNONS ) + { + aCannons[n].Update_NewInput(pos, dir); + return; + } + } + + // if no luck then find a free one + { + int32 n = 0; + while ( n < NUM_WATERCANNONS && 0 != aCannons[n].m_nId ) + n++; + + if ( n < NUM_WATERCANNONS ) + { + aCannons[n].Init(); + aCannons[n].m_nId = id; + aCannons[n].Update_NewInput(pos, dir); + return; + } + } +} + +void CWaterCannons::Update(void) +{ + for ( int32 i = 0; i < NUM_WATERCANNONS; i++ ) + { + if ( aCannons[i].m_nId != 0 ) + aCannons[i].Update_OncePerFrame(i); + } +} + +void CWaterCannons::Render(void) +{ + for ( int32 i = 0; i < NUM_WATERCANNONS; i++ ) + { + if ( aCannons[i].m_nId != 0 ) + aCannons[i].Render(); + } +} + +STARTPATCHES + InjectHook(0x521A30, &CWaterCannon::Init, PATCH_JUMP); + InjectHook(0x521B80, &CWaterCannon::Update_OncePerFrame, PATCH_JUMP); + InjectHook(0x521CC0, &CWaterCannon::Update_NewInput, PATCH_JUMP); + InjectHook(0x521D30, &CWaterCannon::Render, PATCH_JUMP); + InjectHook(0x5220B0, &CWaterCannon::PushPeds, PATCH_JUMP); + InjectHook(0x522440, CWaterCannons::Init, PATCH_JUMP); + InjectHook(0x522470, CWaterCannons::UpdateOne, PATCH_JUMP); + InjectHook(0x522510, CWaterCannons::Update, PATCH_JUMP); + InjectHook(0x522550, CWaterCannons::Render, PATCH_JUMP); + //InjectHook(0x522B40, `global constructor keyed to'watercannon.cpp, PATCH_JUMP); + //InjectHook(0x522B60, CWaterCannon::CWaterCannon, PATCH_JUMP); +ENDPATCHES \ No newline at end of file diff --git a/src/render/WaterCannon.h b/src/render/WaterCannon.h index c2b288f2..826dc78e 100644 --- a/src/render/WaterCannon.h +++ b/src/render/WaterCannon.h @@ -1,15 +1,29 @@ #pragma once +#define WATERCANNON_GRAVITY (0.009f) +#define WATERCANNON_LIFETIME (150) + class CWaterCannon { public: + enum + { + NUM_SEGMENTPOINTS = 16, + }; + int32 m_nId; - int16 m_wIndex; - char gap_6[2]; - int32 m_nTimeCreated; - CVector m_avecPos[16]; - CVector m_avecVelocity[16]; - char m_abUsed[16]; + int16 m_nCur; + char _pad0[2]; + uint32 m_nTimeCreated; + CVector m_avecPos[NUM_SEGMENTPOINTS]; + CVector m_avecVelocity[NUM_SEGMENTPOINTS]; + bool m_abUsed[NUM_SEGMENTPOINTS]; + + void Init(void); + void Update_OncePerFrame(int16 index); + void Update_NewInput(CVector *pos, CVector *dir); + void Render(void); + void PushPeds(void); }; static_assert(sizeof(CWaterCannon) == 412, "CWaterCannon: error"); @@ -17,11 +31,10 @@ static_assert(sizeof(CWaterCannon) == 412, "CWaterCannon: error"); class CWaterCannons { public: - static void Update(); - static void UpdateOne(uint32 id, CVector *pos, CVector *dir); - static void Render(void); + static CWaterCannon aCannons[NUM_WATERCANNONS]; + static void Init(void); -}; - -extern CWaterCannon (&aCannons)[NUM_WATERCANNONS]; - + static void UpdateOne(uint32 id, CVector *pos, CVector *dir); + static void Update(); + static void Render(void); +}; \ No newline at end of file diff --git a/src/skel/win/resource.h b/src/skel/win/resource.h index 2fb3dc50..84dffb95 100644 --- a/src/skel/win/resource.h +++ b/src/skel/win/resource.h @@ -8,6 +8,7 @@ #define IDEXIT 1002 #define IDC_SELECTDEVICE 1005 +#define IDI_MAIN_ICON 1042 // Next default values for new objects // #ifdef APSTUDIO_INVOKED diff --git a/src/skel/win/win.rc b/src/skel/win/win.rc index 676b8ef7..379c473d 100644 --- a/src/skel/win/win.rc +++ b/src/skel/win/win.rc @@ -30,8 +30,18 @@ BEGIN WS_TABSTOP DEFPUSHBUTTON "EXIT",IDEXIT,103,69,52,14 DEFPUSHBUTTON "OK",IDOK,28,69,50,14 - LTEXT "Please select the device to use:",IDC_SELECTDEVICE,7,7, + LTEXT "Please select the Device To Use:",IDC_SELECTDEVICE,7,7, 137,8 END +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_MAIN_ICON ICON DISCARDABLE "gta3.ico" + +///////////////////////////////////////////////////////////////////////////// \ No newline at end of file From a81233429d42753003de65eaab6d5f927d0651bc Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Sat, 28 Mar 2020 18:24:21 +0300 Subject: [PATCH 32/70] WaterCannon uv macros --- src/render/WaterCannon.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/render/WaterCannon.cpp b/src/render/WaterCannon.cpp index 23c2293c..e848fb43 100644 --- a/src/render/WaterCannon.cpp +++ b/src/render/WaterCannon.cpp @@ -29,17 +29,17 @@ void CWaterCannon::Init(void) for ( int32 i = 0; i < NUM_SEGMENTPOINTS; i++ ) m_abUsed[i] = false; - WaterCannonVertices[0].u = 0.0f; - WaterCannonVertices[0].v = 0.0f; + RwIm3DVertexSetU(&WaterCannonVertices[0], 0.0f); + RwIm3DVertexSetV(&WaterCannonVertices[0], 0.0f); - WaterCannonVertices[1].u = 1.0f; - WaterCannonVertices[1].v = 0.0f; + RwIm3DVertexSetU(&WaterCannonVertices[1], 1.0f); + RwIm3DVertexSetV(&WaterCannonVertices[1], 0.0f); - WaterCannonVertices[2].u = 0.0f; - WaterCannonVertices[2].v = 0.0f; + RwIm3DVertexSetU(&WaterCannonVertices[2], 0.0f); + RwIm3DVertexSetV(&WaterCannonVertices[2], 0.0f); - WaterCannonVertices[3].u = 1.0f; - WaterCannonVertices[3].v = 0.0f; + RwIm3DVertexSetU(&WaterCannonVertices[3], 1.0f); + RwIm3DVertexSetV(&WaterCannonVertices[3], 0.0f); WaterCannonIndexList[0] = 0; WaterCannonIndexList[1] = 1; @@ -118,11 +118,11 @@ void CWaterCannon::Render(void) RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)gpWaterRaster); float v = float(CGeneral::GetRandomNumber() & 255) / 256; - - WaterCannonVertices[0].v = v; - WaterCannonVertices[1].v = v; - WaterCannonVertices[2].v = v; - WaterCannonVertices[3].v = v; + + RwIm3DVertexSetV(&WaterCannonVertices[0], v); + RwIm3DVertexSetV(&WaterCannonVertices[1], v); + RwIm3DVertexSetV(&WaterCannonVertices[2], v); + RwIm3DVertexSetV(&WaterCannonVertices[3], v); int16 pointA = m_nCur % -NUM_SEGMENTPOINTS; From 2833a08b9605f71bd478fe91eb18d0a1fb151abc Mon Sep 17 00:00:00 2001 From: aap Date: Sat, 28 Mar 2020 16:29:34 +0100 Subject: [PATCH 33/70] CCam fix --- src/core/Cam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index 4ddde360..546dfde0 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -1530,7 +1530,7 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient CamDist = fBaseDist + Cos(Alpha)*fAngleDist; if(TheCamera.m_bUseTransitionBeta) - Beta = -CGeneral::GetATanOfXY(-Cos(m_fTransitionBeta), -Sin(m_fTransitionBeta)); + Beta = CGeneral::GetATanOfXY(-Cos(m_fTransitionBeta), -Sin(m_fTransitionBeta)); if(TheCamera.m_bCamDirectlyBehind) Beta = TheCamera.m_PedOrientForBehindOrInFront; From 112685ebac0a4f5da50fdc60ff71c35a5cddbdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Sat, 28 Mar 2020 17:47:52 +0300 Subject: [PATCH 34/70] CCopPed done and #include cleanup --- README.md | 5 +- src/audio/AudioManager.cpp | 1 + src/audio/AudioScriptObject.cpp | 1 + src/audio/DMAudio.h | 1 - src/audio/MusicManager.cpp | 1 + src/audio/PoliceRadio.h | 2 + src/control/Bridge.h | 3 +- src/control/CarAI.cpp | 3 + src/control/CarCtrl.cpp | 3 +- src/control/Darkel.cpp | 1 + src/control/Darkel.h | 2 +- src/control/GameLogic.cpp | 1 + src/control/Gangs.cpp | 7 +- src/control/Gangs.h | 6 +- src/control/Phones.cpp | 1 + src/control/Pickups.cpp | 3 + src/control/Replay.cpp | 5 + src/control/Replay.h | 10 +- src/control/Script.cpp | 3 + src/core/AnimViewer.cpp | 5 + src/core/CutsceneMgr.cpp | 2 + src/core/EventList.cpp | 2 +- src/core/Frontend.cpp | 21 ++-- src/core/Game.cpp | 2 + src/core/Pad.cpp | 3 +- src/core/PlayerInfo.cpp | 3 + src/core/Streaming.cpp | 1 + src/objects/Object.cpp | 1 + src/objects/Object.h | 2 +- src/peds/CivilianPed.cpp | 2 + src/peds/CopPed.cpp | 206 ++++++++++++++++++++++++++++++-- src/peds/CopPed.h | 4 +- src/peds/EmergencyPed.cpp | 1 + src/peds/Ped.cpp | 12 +- src/peds/Ped.h | 11 +- src/peds/PedIK.cpp | 2 +- src/peds/PedIK.h | 3 +- src/peds/PedPlacement.cpp | 1 + src/peds/PedPlacement.h | 2 - src/peds/PlayerPed.cpp | 5 + src/peds/PlayerPed.h | 6 +- src/peds/Population.cpp | 10 +- src/peds/Population.h | 4 +- src/render/Hud.cpp | 1 + src/vehicles/Automobile.cpp | 1 + src/vehicles/Heli.cpp | 2 + src/vehicles/Plane.cpp | 2 + src/vehicles/Train.cpp | 1 + src/vehicles/Vehicle.cpp | 1 + src/weapons/WeaponInfo.cpp | 2 + src/weapons/WeaponInfo.h | 7 +- 51 files changed, 318 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 3c394e62..85014cc1 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,12 @@ to reverse at the time, calling the original functions is acceptable. ### Unreversed / incomplete classes (at least the ones we know) ``` -cAudioManager - being worked on +cAudioManager - WIP CBoat CBrightLights CBulletInfo CBulletTraces CCamera -CCopPed CCrane CCranes CCullZone @@ -57,7 +56,7 @@ CGame CGarage CGarages CGlass -CMenuManager +CMenuManager - WIP CMotionBlurStreaks CObject CPacManPickups diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 539c9e91..7f780cf6 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -20,6 +20,7 @@ #include "MusicManager.h" #include "Pad.h" #include "Ped.h" +#include "Fire.h" #include "Physical.h" #include "Placeable.h" #include "Plane.h" diff --git a/src/audio/AudioScriptObject.cpp b/src/audio/AudioScriptObject.cpp index 796cd88b..b5093c52 100644 --- a/src/audio/AudioScriptObject.cpp +++ b/src/audio/AudioScriptObject.cpp @@ -2,6 +2,7 @@ #include "patcher.h" #include "AudioScriptObject.h" #include "Pools.h" +#include "DMAudio.h" WRAPPER void cAudioScriptObject::SaveAllAudioScriptObjects(uint8 *buf, uint32 *size) { EAXJMP(0x57c460); } diff --git a/src/audio/DMAudio.h b/src/audio/DMAudio.h index 125263f0..c792b414 100644 --- a/src/audio/DMAudio.h +++ b/src/audio/DMAudio.h @@ -1,7 +1,6 @@ #pragma once #include "audio_enums.h" -#include "Wanted.h" enum eSound : int16 { diff --git a/src/audio/MusicManager.cpp b/src/audio/MusicManager.cpp index 1fac8a23..1f1c343a 100644 --- a/src/audio/MusicManager.cpp +++ b/src/audio/MusicManager.cpp @@ -8,6 +8,7 @@ #include "Hud.h" #include "ModelIndices.h" #include "Replay.h" +#include "Pad.h" #include "Text.h" #include "Timer.h" #include "World.h" diff --git a/src/audio/PoliceRadio.h b/src/audio/PoliceRadio.h index 152a5ee2..4c7030f1 100644 --- a/src/audio/PoliceRadio.h +++ b/src/audio/PoliceRadio.h @@ -1,5 +1,7 @@ #pragma once +#include "Wanted.h" + struct cAMCrime { int32 type; CVector position; diff --git a/src/control/Bridge.h b/src/control/Bridge.h index 377c8bf8..63f41578 100644 --- a/src/control/Bridge.h +++ b/src/control/Bridge.h @@ -1,5 +1,6 @@ #pragma once -#include "Entity.h" + +class CEntity; enum bridgeStates { STATE_BRIDGE_LOCKED, diff --git a/src/control/CarAI.cpp b/src/control/CarAI.cpp index c5d62c48..e47e3d5e 100644 --- a/src/control/CarAI.cpp +++ b/src/control/CarAI.cpp @@ -9,6 +9,9 @@ #include "HandlingMgr.h" #include "ModelIndices.h" #include "PlayerPed.h" +#include "Wanted.h" +#include "DMAudio.h" +#include "Fire.h" #include "Pools.h" #include "Timer.h" #include "TrafficLights.h" diff --git a/src/control/CarCtrl.cpp b/src/control/CarCtrl.cpp index de8c799e..07ba2e3c 100644 --- a/src/control/CarCtrl.cpp +++ b/src/control/CarCtrl.cpp @@ -19,6 +19,7 @@ #include "Ped.h" #include "PlayerInfo.h" #include "PlayerPed.h" +#include "Wanted.h" #include "Pools.h" #include "Renderer.h" #include "RoadBlocks.h" @@ -27,7 +28,7 @@ #include "Streaming.h" #include "VisibilityPlugins.h" #include "Vehicle.h" -#include "Wanted.h" +#include "Fire.h" #include "World.h" #include "Zones.h" diff --git a/src/control/Darkel.cpp b/src/control/Darkel.cpp index b7ae0726..ec1b887e 100644 --- a/src/control/Darkel.cpp +++ b/src/control/Darkel.cpp @@ -3,6 +3,7 @@ #include "main.h" #include "Darkel.h" #include "PlayerPed.h" +#include "Wanted.h" #include "Timer.h" #include "DMAudio.h" #include "Population.h" diff --git a/src/control/Darkel.h b/src/control/Darkel.h index f17d7581..12ce4451 100644 --- a/src/control/Darkel.h +++ b/src/control/Darkel.h @@ -1,9 +1,9 @@ #pragma once -#include "Weapon.h" #include "ModelIndices.h" class CVehicle; class CPed; +enum eWeaponType; enum { diff --git a/src/control/GameLogic.cpp b/src/control/GameLogic.cpp index 1e5b72c3..1493cec0 100644 --- a/src/control/GameLogic.cpp +++ b/src/control/GameLogic.cpp @@ -9,6 +9,7 @@ #include "CutsceneMgr.h" #include "World.h" #include "PlayerPed.h" +#include "Wanted.h" #include "Camera.h" #include "Messages.h" #include "CarCtrl.h" diff --git a/src/control/Gangs.cpp b/src/control/Gangs.cpp index f9cb4698..340fe0f6 100644 --- a/src/control/Gangs.cpp +++ b/src/control/Gangs.cpp @@ -1,7 +1,8 @@ #include "common.h" #include "patcher.h" #include "ModelIndices.h" -#include "Gangs.h" +#include "Gangs.h" +#include "Weapon.h" //CGangInfo(&CGangs::Gang)[NUM_GANGS] = *(CGangInfo(*)[NUM_GANGS])*(uintptr*)0x6EDF78; CGangInfo CGangs::Gang[NUM_GANGS]; @@ -38,8 +39,8 @@ void CGangs::SetGangVehicleModel(int16 gang, int32 model) void CGangs::SetGangWeapons(int16 gang, int32 weapon1, int32 weapon2) { CGangInfo *gi = GetGangInfo(gang); - gi->m_Weapon1 = (eWeaponType)weapon1; - gi->m_Weapon2 = (eWeaponType)weapon2; + gi->m_Weapon1 = weapon1; + gi->m_Weapon2 = weapon2; } void CGangs::SetGangPedModelOverride(int16 gang, int8 ovrd) diff --git a/src/control/Gangs.h b/src/control/Gangs.h index a348f259..cf22cc73 100644 --- a/src/control/Gangs.h +++ b/src/control/Gangs.h @@ -1,13 +1,11 @@ #pragma once -#include "Weapon.h" - struct CGangInfo { int32 m_nVehicleMI; int8 m_nPedModelOverride; - eWeaponType m_Weapon1; - eWeaponType m_Weapon2; + int32 m_Weapon1; + int32 m_Weapon2; CGangInfo(); }; diff --git a/src/control/Phones.cpp b/src/control/Phones.cpp index f3b3a8db..276f02b9 100644 --- a/src/control/Phones.cpp +++ b/src/control/Phones.cpp @@ -11,6 +11,7 @@ #include "General.h" #include "AudioScriptObject.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" CPhoneInfo &gPhoneInfo = *(CPhoneInfo*)0x732A20; diff --git a/src/control/Pickups.cpp b/src/control/Pickups.cpp index 53da89f4..b1832f0e 100644 --- a/src/control/Pickups.cpp +++ b/src/control/Pickups.cpp @@ -15,6 +15,9 @@ #include "Pad.h" #include "Pickups.h" #include "PlayerPed.h" +#include "Wanted.h" +#include "DMAudio.h" +#include "Fire.h" #include "PointLights.h" #include "Pools.h" #include "Script.h" diff --git a/src/control/Replay.cpp b/src/control/Replay.cpp index a68dd5e7..3c0393aa 100644 --- a/src/control/Replay.cpp +++ b/src/control/Replay.cpp @@ -5,6 +5,7 @@ #include "SpecialFX.h" #include "CarCtrl.h" #include "CivilianPed.h" +#include "Wanted.h" #include "Clock.h" #include "DMAudio.h" #include "Draw.h" @@ -22,6 +23,8 @@ #include "Pools.h" #include "Population.h" #include "Replay.h" +#include "References.h" +#include "Pools.h" #include "RpAnimBlend.h" #include "RwHelper.h" #include "CutsceneMgr.h" @@ -33,6 +36,8 @@ #include "Zones.h" #include "Font.h" #include "Text.h" +#include "Camera.h" +#include "Radar.h" uint8 &CReplay::Mode = *(uint8*)0x95CD5B; CAddressInReplayBuffer &CReplay::Record = *(CAddressInReplayBuffer*)0x942F7C; diff --git a/src/control/Replay.h b/src/control/Replay.h index cc652a11..56de52a3 100644 --- a/src/control/Replay.h +++ b/src/control/Replay.h @@ -1,14 +1,7 @@ #pragma once -#include "Camera.h" -#include "Ped.h" #include "Pools.h" -#include "Radar.h" -#include "References.h" -#include "Vehicle.h" -#include "Wanted.h" #include "World.h" -#include "common.h" #ifdef FIX_BUGS #ifndef DONT_FIX_REPLAY_BUGS @@ -16,6 +9,9 @@ #endif #endif +class CVehicle; +struct CReference; + struct CAddressInReplayBuffer { uint32 m_nOffset; diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 2cfd2a9b..14f55734 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -53,6 +53,8 @@ #include "Restart.h" #include "Replay.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" +#include "Fire.h" #include "Rubbish.h" #include "Shadows.h" #include "SpecialFX.h" @@ -65,6 +67,7 @@ #include "Weather.h" #include "World.h" #include "Zones.h" +#include "Radar.h" #define PICKUP_PLACEMENT_OFFSET 0.5f #define PED_FIND_Z_OFFSET 5.0f diff --git a/src/core/AnimViewer.cpp b/src/core/AnimViewer.cpp index a2d7b94a..20a0098d 100644 --- a/src/core/AnimViewer.cpp +++ b/src/core/AnimViewer.cpp @@ -33,6 +33,7 @@ #include "Clock.h" #include "Timecycle.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" #include "Shadows.h" #include "Radar.h" #include "Hud.h" @@ -207,6 +208,7 @@ PlayAnimation(RpClump *clump, AssocGroupId animGroup, AnimationId anim) animAssoc->SetRun(); } +extern void (*DebugMenuProcess)(void); void CAnimViewer::Update(void) { @@ -246,6 +248,9 @@ CAnimViewer::Update(void) } CPad::UpdatePads(); CPad* pad = CPad::GetPad(0); + + DebugMenuProcess(); + CStreaming::UpdateForAnimViewer(); CStreaming::RequestModel(modelId, 0); if (CStreaming::HasModelLoaded(modelId)) { diff --git a/src/core/CutsceneMgr.cpp b/src/core/CutsceneMgr.cpp index c13aa3a8..a3ff2fd0 100644 --- a/src/core/CutsceneMgr.cpp +++ b/src/core/CutsceneMgr.cpp @@ -9,12 +9,14 @@ #include "FileMgr.h" #include "main.h" #include "AnimManager.h" +#include "AnimBlendAssociation.h" #include "AnimBlendAssocGroup.h" #include "AnimBlendClumpData.h" #include "Pad.h" #include "DMAudio.h" #include "World.h" #include "PlayerPed.h" +#include "Wanted.h" #include "CutsceneHead.h" #include "RpAnimBlend.h" #include "ModelIndices.h" diff --git a/src/core/EventList.cpp b/src/core/EventList.cpp index 4364359a..d72e32c4 100644 --- a/src/core/EventList.cpp +++ b/src/core/EventList.cpp @@ -212,7 +212,7 @@ CEventList::ReportCrimeForEvent(eEventType type, int32 crimeId, bool copsDontCar #ifdef VC_PED_PORTS if (crime == CRIME_HIT_PED && ((CPed*)crimeId)->IsPointerValid() && - FindPlayerPed()->m_pWanted->m_nWantedLevel == 0 && ((CPed*)crimeId)->m_ped_flagE2) { + FindPlayerPed()->m_pWanted->m_nWantedLevel == 0 && ((CPed*)crimeId)->bBeingChasedByPolice) { if(!((CPed*)crimeId)->DyingOrDead()) { sprintf(gString, "$50 Good Citizen Bonus!"); diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index aff8a3ec..0bade6c7 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -94,7 +94,6 @@ int32 *&pControlEdit = *(int32**)0x628D08; bool &DisplayComboButtonErrMsg = *(bool*)0x628D14; int32 &MouseButtonJustClicked = *(int32*)0x628D0C; int32 &JoyButtonJustClicked = *(int32*)0x628D10; -uint32 &nTimeForSomething = *(uint32*)0x628D54; bool &holdingScrollBar = *(bool*)0x628D59; //int32 *pControlTemp = 0; @@ -2202,15 +2201,15 @@ CMenuManager::ProcessButtonPresses(void) field_535 = false; } - static int nTimeForSomething = 0; + static uint32 lastTimeClickedScrollButton = 0; - if (CTimer::GetTimeInMillisecondsPauseMode() - nTimeForSomething >= 200) { + if (CTimer::GetTimeInMillisecondsPauseMode() - lastTimeClickedScrollButton >= 200) { m_bPressedPgUpOnList = false; m_bPressedPgDnOnList = false; m_bPressedUpOnList = false; m_bPressedDownOnList = false; m_bPressedScrollButton = false; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); } if (CPad::GetPad(0)->GetTabJustDown()) { @@ -2249,7 +2248,7 @@ CMenuManager::ProcessButtonPresses(void) m_nCurrExLayer = 19; if (!m_bPressedUpOnList) { m_bPressedUpOnList = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); ScrollUpListByOne(); } @@ -2271,7 +2270,7 @@ CMenuManager::ProcessButtonPresses(void) m_nCurrExLayer = 19; if (!m_bPressedDownOnList) { m_bPressedDownOnList = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); ScrollDownListByOne(); } @@ -2286,7 +2285,7 @@ CMenuManager::ProcessButtonPresses(void) m_nCurrExLayer = 19; if (!m_bPressedPgUpOnList) { m_bPressedPgUpOnList = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); m_bShowMouse = false; DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); PageUpList(false); @@ -2298,7 +2297,7 @@ CMenuManager::ProcessButtonPresses(void) m_nCurrExLayer = 19; if (!m_bPressedPgDnOnList) { m_bPressedPgDnOnList = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); m_bShowMouse = false; DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_DENIED, 0); PageDownList(false); @@ -2384,7 +2383,7 @@ CMenuManager::ProcessButtonPresses(void) case HOVEROPTION_CLICKED_SCROLL_UP: if (!m_bPressedScrollButton) { m_bPressedScrollButton = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); ScrollUpListByOne(); } break; @@ -2392,7 +2391,7 @@ CMenuManager::ProcessButtonPresses(void) case HOVEROPTION_CLICKED_SCROLL_DOWN: if (!m_bPressedScrollButton) { m_bPressedScrollButton = true; - nTimeForSomething = CTimer::GetTimeInMillisecondsPauseMode(); + lastTimeClickedScrollButton = CTimer::GetTimeInMillisecondsPauseMode(); ScrollDownListByOne(); } break; @@ -3593,7 +3592,7 @@ void CMenuManager::SwitchMenuOnAndOff() PcSaveHelper.PopulateSlotInfo(); m_nCurrOption = 0; } -/* // PS2 leftover? +/* // PS2 leftover if (m_nCurrScreen != MENUPAGE_SOUND_SETTINGS && gMusicPlaying) { DMAudio.StopFrontEndTrack(); diff --git a/src/core/Game.cpp b/src/core/Game.cpp index e89d62a0..fce0c67f 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -41,6 +41,8 @@ #include "Record.h" #include "Renderer.h" #include "Replay.h" +#include "References.h" +#include "Radar.h" #include "Restart.h" #include "RoadBlocks.h" #include "PedRoutes.h" diff --git a/src/core/Pad.cpp b/src/core/Pad.cpp index 87f45b9f..6bbe00f2 100644 --- a/src/core/Pad.cpp +++ b/src/core/Pad.cpp @@ -574,8 +574,9 @@ void CPad::AffectFromXinput(uint32 pad) PCTempJoyState.RightShoulder2 = xstate.Gamepad.bRightTrigger; PCTempJoyState.Select = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? 255 : 0; +#ifdef REGISTER_START_BUTTON PCTempJoyState.Start = (xstate.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? 255 : 0; - +#endif float lx = (float)xstate.Gamepad.sThumbLX / (float)0x7FFF; float ly = (float)xstate.Gamepad.sThumbLY / (float)0x7FFF; float rx = (float)xstate.Gamepad.sThumbRX / (float)0x7FFF; diff --git a/src/core/PlayerInfo.cpp b/src/core/PlayerInfo.cpp index e0c0259e..ead32ee7 100644 --- a/src/core/PlayerInfo.cpp +++ b/src/core/PlayerInfo.cpp @@ -2,7 +2,9 @@ #include "patcher.h" #include "main.h" #include "PlayerPed.h" +#include "Wanted.h" #include "PlayerInfo.h" +#include "Fire.h" #include "Frontend.h" #include "PlayerSkin.h" #include "Darkel.h" @@ -12,6 +14,7 @@ #include "Remote.h" #include "World.h" #include "Replay.h" +#include "Camera.h" #include "Pad.h" #include "ProjectileInfo.h" #include "Explosion.h" diff --git a/src/core/Streaming.cpp b/src/core/Streaming.cpp index 6106f3df..3dcb767a 100644 --- a/src/core/Streaming.cpp +++ b/src/core/Streaming.cpp @@ -10,6 +10,7 @@ #include "TxdStore.h" #include "ModelIndices.h" #include "Pools.h" +#include "Wanted.h" #include "Directory.h" #include "RwHelper.h" #include "World.h" diff --git a/src/objects/Object.cpp b/src/objects/Object.cpp index 809ba971..89959975 100644 --- a/src/objects/Object.cpp +++ b/src/objects/Object.cpp @@ -5,6 +5,7 @@ #include "Pools.h" #include "Radar.h" #include "Object.h" +#include "DummyObject.h" WRAPPER void CObject::ObjectDamage(float amount) { EAXJMP(0x4BB240); } WRAPPER void CObject::DeleteAllTempObjectInArea(CVector, float) { EAXJMP(0x4BBED0); } diff --git a/src/objects/Object.h b/src/objects/Object.h index b9c570f5..9fcf9c0c 100644 --- a/src/objects/Object.h +++ b/src/objects/Object.h @@ -1,7 +1,6 @@ #pragma once #include "Physical.h" -#include "DummyObject.h" enum { GAME_OBJECT = 1, @@ -26,6 +25,7 @@ enum { }; class CVehicle; +class CDummyObject; class CObject : public CPhysical { diff --git a/src/peds/CivilianPed.cpp b/src/peds/CivilianPed.cpp index 2e6166be..533d7c98 100644 --- a/src/peds/CivilianPed.cpp +++ b/src/peds/CivilianPed.cpp @@ -4,6 +4,8 @@ #include "Phones.h" #include "General.h" #include "PlayerPed.h" +#include "Wanted.h" +#include "DMAudio.h" #include "World.h" #include "Vehicle.h" #include "SurfaceTable.h" diff --git a/src/peds/CopPed.cpp b/src/peds/CopPed.cpp index 94acac05..b5812136 100644 --- a/src/peds/CopPed.cpp +++ b/src/peds/CopPed.cpp @@ -3,15 +3,19 @@ #include "World.h" #include "PlayerPed.h" #include "CopPed.h" +#include "Wanted.h" +#include "DMAudio.h" #include "ModelIndices.h" #include "Vehicle.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" #include "General.h" #include "ZoneCull.h" #include "PathFind.h" #include "RoadBlocks.h" - -WRAPPER void CCopPed::ProcessControl() { EAXJMP(0x4C1400); } +#include "CarCtrl.h" +#include "Renderer.h" +#include "Camera.h" CCopPed::CCopPed(eCopType copType) : CPed(PEDTYPE_COP) { @@ -62,12 +66,12 @@ CCopPed::CCopPed(eCopType copType) : CPed(PEDTYPE_COP) field_1356 = 0; m_attackTimer = 0; m_bBeatingSuspect = false; - m_bZoneDisabledButClose = false; + m_bStopAndShootDisabledZone = false; m_bZoneDisabled = false; field_1364 = -1; m_pPointGunAt = nil; - // VC also initializes in here, but it keeps object + // VC also initializes in here, but as nil #ifdef FIX_BUGS m_wRoadblockNode = -1; #endif @@ -171,7 +175,7 @@ CCopPed::ClearPursuit(void) bIsRunning = false; bNotAllowedToDuck = false; bKindaStayInSamePlace = false; - m_bZoneDisabledButClose = false; + m_bStopAndShootDisabledZone = false; m_bZoneDisabled = false; ClearObjective(); if (IsPedInControl()) { @@ -213,7 +217,7 @@ CCopPed::SetPursuit(bool ignoreCopLimit) SetObjectiveTimer(0); bNotAllowedToDuck = true; bIsRunning = true; - m_bZoneDisabledButClose = false; + m_bStopAndShootDisabledZone = false; } } } @@ -315,13 +319,15 @@ CCopPed::CopAI(void) m_prevObjective = OBJECTIVE_NONE; m_nLastPedState = PED_NONE; SetAttackTimer(0); + + // Safe distance for disabled zone? Or to just make game easier? if (m_fDistanceToTarget > 15.0f) - m_bZoneDisabledButClose = true; + m_bStopAndShootDisabledZone = true; } } else if (m_bZoneDisabled && !CCullZones::NoPolice()) { m_bZoneDisabled = false; m_bIsDisabledCop = false; - m_bZoneDisabledButClose = false; + m_bStopAndShootDisabledZone = false; bKindaStayInSamePlace = false; bCrouchWhenShooting = false; bDuckAndCover = false; @@ -524,7 +530,7 @@ CCopPed::CopAI(void) if (!anotherCopChasesHim) { SetObjective(OBJECTIVE_KILL_CHAR_ON_FOOT, nearPed); nearPed->SetObjective(OBJECTIVE_FLEE_CHAR_ON_FOOT_TILL_SAFE, this); - nearPed->m_ped_flagE2 = true; + nearPed->bBeingChasedByPolice = true; return; } } @@ -551,6 +557,186 @@ CCopPed::CopAI(void) } } +void +CCopPed::ProcessControl(void) +{ + if (m_nZoneLevel > LEVEL_NONE && m_nZoneLevel != CCollision::ms_collisionInMemory) + return; + + CPed::ProcessControl(); + if (bWasPostponed) + return; + + if (m_nPedState == PED_DEAD) { + ClearPursuit(); + m_objective = OBJECTIVE_NONE; + return; + } + if (m_nPedState == PED_DIE) + return; + + if (m_nPedState == PED_ARREST_PLAYER) { + ArrestPlayer(); + return; + } + GetWeapon()->Update(m_audioEntityId); + if (m_moved.Magnitude() > 0.0f) + Avoid(); + + CPhysical *playerOrHisVeh = FindPlayerVehicle() ? (CPhysical*)FindPlayerVehicle() : (CPhysical*)FindPlayerPed(); + CPlayerPed *player = FindPlayerPed(); + + m_fDistanceToTarget = (playerOrHisVeh->GetPosition() - GetPosition()).Magnitude(); + if (player->m_nPedState == PED_ARRESTED || player->DyingOrDead()) { + if (m_fDistanceToTarget < 5.0f) { + SetArrestPlayer(player); + return; + } + if (IsPedInControl()) + SetIdle(); + } + if (m_bIsInPursuit) { + if (player->m_nPedState != PED_ARRESTED && !player->DyingOrDead()) { + switch (m_nCopType) { + case COP_FBI: + Say(SOUND_PED_PURSUIT_FBI); + break; + case COP_SWAT: + Say(SOUND_PED_PURSUIT_SWAT); + break; + case COP_ARMY: + Say(SOUND_PED_PURSUIT_ARMY); + break; + default: + Say(SOUND_PED_PURSUIT_COP); + break; + } + } + } + + if (IsPedInControl()) { + CopAI(); + /* switch (m_nCopType) + { + case COP_FBI: + CopAI(); + break; + case COP_SWAT: + CopAI(); + break; + case COP_ARMY: + CopAI(); + break; + default: + CopAI(); + break; + } */ + } else if (InVehicle()) { + if (m_pMyVehicle->pDriver == this && m_pMyVehicle->AutoPilot.m_nCarMission == MISSION_NONE && + CanPedDriveOff() && m_pMyVehicle->VehicleCreatedBy != MISSION_VEHICLE) { + + CCarCtrl::JoinCarWithRoadSystem(m_pMyVehicle); + m_pMyVehicle->AutoPilot.m_nCarMission = MISSION_CRUISE; + m_pMyVehicle->AutoPilot.m_nDrivingStyle = DRIVINGSTYLE_STOP_FOR_CARS; + m_pMyVehicle->AutoPilot.m_nCruiseSpeed = 17; + } + } + if (IsPedInControl() || m_nPedState == PED_DRIVING) + ScanForCrimes(); + + // They may have used goto to jump here in case of PED_ATTACK. + if (m_nPedState == PED_IDLE || m_nPedState == PED_ATTACK) { + if (m_objective == OBJECTIVE_KILL_CHAR_ON_FOOT && + player && player->EnteringCar() && m_fDistanceToTarget < 1.3f) { + SetArrestPlayer(player); + } + } else { + if (m_nPedState == PED_SEEK_POS) { + if (player->m_nPedState == PED_ARRESTED) { + SetIdle(); + SetLookFlag(player, false); + SetLookTimer(1000); + RestorePreviousObjective(); + } else { + if (player->m_pMyVehicle && player->m_pMyVehicle->m_nNumGettingIn != 0) { + // This is 1.3f when arresting in car without seeking first (in above) +#if defined(VC_PED_PORTS) || defined(FIX_BUGS) + m_distanceToCountSeekDone = 1.3f; +#else + m_distanceToCountSeekDone = 2.0f; +#endif + } + + if (bDuckAndCover) { + if (!bNotAllowedToDuck && Seek()) { + SetMoveState(PEDMOVE_STILL); + SetMoveAnim(); + SetPointGunAt(m_pedInObjective); + } + } else if (Seek()) { + CVehicle *playerVeh = FindPlayerVehicle(); + if (!playerVeh && player && player->EnteringCar()) { + SetArrestPlayer(player); + } else if (1.5f + GetPosition().z <= m_vecSeekPos.z || GetPosition().z - 0.3f >= m_vecSeekPos.z) { + SetMoveState(PEDMOVE_STILL); + } else if (playerVeh && playerVeh->CanPedEnterCar() && playerVeh->m_nNumGettingIn == 0) { + SetCarJack(playerVeh); + } + } + } + } else if (m_nPedState == PED_SEEK_ENTITY) { + if (!m_pSeekTarget) { + RestorePreviousState(); + } else { + m_vecSeekPos = m_pSeekTarget->GetPosition(); + if (Seek()) { + if (m_objective == OBJECTIVE_KILL_CHAR_ON_FOOT && m_fDistanceToTarget < 2.5f && player) { + if (player->m_nPedState == PED_ARRESTED || player->m_nPedState == PED_ENTER_CAR || + (player->m_nPedState == PED_CARJACK && m_fDistanceToTarget < 1.3f)) { + SetArrestPlayer(player); + } else + RestorePreviousState(); + } else { + RestorePreviousState(); + } + + } + } + } + } + if (!m_bStopAndShootDisabledZone) + return; + + bool dontShoot = false; + if (GetIsOnScreen() && CRenderer::IsEntityCullZoneVisible(this)) { + if (((CTimer::GetFrameCounter() + m_randomSeed) & 0x1F) == 17) { + CEntity *foundBuilding = nil; + CColPoint foundCol; + CVector lookPos = GetPosition() + CVector(0.0f, 0.0f, 0.7f); + CVector camPos = TheCamera.GetGameCamPosition(); + CWorld::ProcessLineOfSight(camPos, lookPos, foundCol, foundBuilding, + true, false, false, false, false, false, false); + + // He's at least 15.0 far, in disabled zone, collided into somewhere (that's why m_bStopAndShootDisabledZone set), + // and now has building on front of him. He's stupid, we don't need him. + if (foundBuilding) { + FlagToDestroyWhenNextProcessed(); + dontShoot = true; + } + } + } else { + FlagToDestroyWhenNextProcessed(); + dontShoot = true; + } + + if (!dontShoot) { + bStopAndShoot = true; + bKindaStayInSamePlace = true; + bIsPointingGunAt = true; + SetAttack(m_pedInObjective); + } +} + #include class CCopPed_ : public CCopPed @@ -558,11 +744,13 @@ class CCopPed_ : public CCopPed public: CCopPed *ctor(eCopType type) { return ::new (this) CCopPed(type); }; void dtor(void) { CCopPed::~CCopPed(); } + void ProcessControl_(void) { CCopPed::ProcessControl(); } }; STARTPATCHES InjectHook(0x4C11B0, &CCopPed_::ctor, PATCH_JUMP); InjectHook(0x4C13E0, &CCopPed_::dtor, PATCH_JUMP); + InjectHook(0x4C1400, &CCopPed_::ProcessControl_, PATCH_JUMP); InjectHook(0x4C28C0, &CCopPed::ClearPursuit, PATCH_JUMP); InjectHook(0x4C2B00, &CCopPed::SetArrestPlayer, PATCH_JUMP); InjectHook(0x4C27D0, &CCopPed::SetPursuit, PATCH_JUMP); diff --git a/src/peds/CopPed.h b/src/peds/CopPed.h index 142be56a..625cae49 100644 --- a/src/peds/CopPed.h +++ b/src/peds/CopPed.h @@ -17,10 +17,10 @@ public: int8 field_1343; float m_fDistanceToTarget; int8 m_bIsInPursuit; - int8 m_bIsDisabledCop; // What disabled cop actually is? + int8 m_bIsDisabledCop; int8 field_1350; bool m_bBeatingSuspect; - int8 m_bZoneDisabledButClose; + int8 m_bStopAndShootDisabledZone; int8 m_bZoneDisabled; int8 field_1354; int8 field_1355; diff --git a/src/peds/EmergencyPed.cpp b/src/peds/EmergencyPed.cpp index 0d27a532..3a5067e7 100644 --- a/src/peds/EmergencyPed.cpp +++ b/src/peds/EmergencyPed.cpp @@ -1,6 +1,7 @@ #include "common.h" #include "patcher.h" #include "EmergencyPed.h" +#include "DMAudio.h" #include "ModelIndices.h" #include "Vehicle.h" #include "Fire.h" diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index 05cac3a7..8b83d976 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -7,13 +7,21 @@ #include "World.h" #include "RpAnimBlend.h" #include "Ped.h" +#include "Wanted.h" #include "PlayerPed.h" +#include "PedType.h" +#include "AnimBlendClumpData.h" +#include "AnimBlendAssociation.h" +#include "Fire.h" +#include "DMAudio.h" #include "General.h" #include "SurfaceTable.h" #include "VisibilityPlugins.h" #include "AudioManager.h" #include "HandlingMgr.h" #include "Replay.h" +#include "Camera.h" +#include "Radar.h" #include "PedPlacement.h" #include "Shadows.h" #include "Weather.h" @@ -552,7 +560,7 @@ CPed::CPed(uint32 pedType) : m_pedIK(this) bScriptObjectiveCompleted = false; bKindaStayInSamePlace = false; - m_ped_flagE2 = false; + bBeingChasedByPolice = false; bNotAllowedToDuck = false; bCrouchWhenShooting = false; bIsDucking = false; @@ -9597,7 +9605,7 @@ CPed::ProcessControl(void) float neededTurnToCriminal = angleToLookCriminal - angleToFace; if (neededTurnToCriminal > DEGTORAD(150.0f) && neededTurnToCriminal < DEGTORAD(210.0f)) { - ((CCopPed*)this)->m_bZoneDisabledButClose = true; + ((CCopPed*)this)->m_bStopAndShootDisabledZone = true; } } } diff --git a/src/peds/Ped.h b/src/peds/Ped.h index a19dc9f0..2edd5d68 100644 --- a/src/peds/Ped.h +++ b/src/peds/Ped.h @@ -3,14 +3,9 @@ #include "Physical.h" #include "Weapon.h" #include "PedStats.h" -#include "PedType.h" #include "PedIK.h" #include "AnimManager.h" -#include "AnimBlendClumpData.h" -#include "AnimBlendAssociation.h" #include "WeaponInfo.h" -#include "Fire.h" -#include "DMAudio.h" #include "EventList.h" #define FEET_OFFSET 1.04f @@ -19,6 +14,10 @@ struct CPathNode; class CAccident; class CObject; +class CFire; +struct AnimBlendFrameData; +class CAnimBlendAssociation; +enum eCrimeType; struct PedAudioData { @@ -339,7 +338,7 @@ public: uint8 bScriptObjectiveCompleted : 1; uint8 bKindaStayInSamePlace : 1; - uint8 m_ped_flagE2 : 1; // bBeingChasedByPolice? + uint8 bBeingChasedByPolice : 1; // Unused VC leftover. Should've been set for criminal/gang members uint8 bNotAllowedToDuck : 1; uint8 bCrouchWhenShooting : 1; uint8 bIsDucking : 1; diff --git a/src/peds/PedIK.cpp b/src/peds/PedIK.cpp index 38ab429e..cc4b0dd0 100644 --- a/src/peds/PedIK.cpp +++ b/src/peds/PedIK.cpp @@ -80,7 +80,7 @@ CPedIK::RotateTorso(AnimBlendFrameData *animBlend, LimbOrientation *limb, bool c } void -CPedIK::GetComponentPosition(RwV3d *pos, PedNode node) +CPedIK::GetComponentPosition(RwV3d *pos, uint32 node) { RwFrame *f; RwMatrix *mat; diff --git a/src/peds/PedIK.h b/src/peds/PedIK.h index dc3f8dda..df9017f3 100644 --- a/src/peds/PedIK.h +++ b/src/peds/PedIK.h @@ -1,6 +1,5 @@ #pragma once #include "common.h" -#include "PedModelInfo.h" #include "AnimBlendClumpData.h" struct LimbOrientation @@ -52,7 +51,7 @@ public: bool PointGunInDirection(float phi, float theta); bool PointGunInDirectionUsingArm(float phi, float theta); bool PointGunAtPosition(CVector const& position); - void GetComponentPosition(RwV3d *pos, PedNode node); + void GetComponentPosition(RwV3d *pos, uint32 node); static RwMatrix *GetWorldMatrix(RwFrame *source, RwMatrix *destination); void RotateTorso(AnimBlendFrameData* animBlend, LimbOrientation* limb, bool changeRoll); void ExtractYawAndPitchLocal(RwMatrixTag *mat, float *yaw, float *pitch); diff --git a/src/peds/PedPlacement.cpp b/src/peds/PedPlacement.cpp index b22e1d58..e5f6a077 100644 --- a/src/peds/PedPlacement.cpp +++ b/src/peds/PedPlacement.cpp @@ -1,5 +1,6 @@ #include "common.h" #include "patcher.h" +#include "Ped.h" #include "PedPlacement.h" #include "World.h" diff --git a/src/peds/PedPlacement.h b/src/peds/PedPlacement.h index b1b5be93..6ba4ae71 100644 --- a/src/peds/PedPlacement.h +++ b/src/peds/PedPlacement.h @@ -1,7 +1,5 @@ #pragma once -#include "Ped.h" - class CPedPlacement { public: static void FindZCoorForPed(CVector* pos); diff --git a/src/peds/PlayerPed.cpp b/src/peds/PlayerPed.cpp index 49d0183e..5275f716 100644 --- a/src/peds/PlayerPed.cpp +++ b/src/peds/PlayerPed.cpp @@ -1,11 +1,16 @@ #include "common.h" #include "patcher.h" #include "PlayerPed.h" +#include "Wanted.h" +#include "Fire.h" +#include "DMAudio.h" +#include "Pad.h" #include "Camera.h" #include "WeaponEffects.h" #include "ModelIndices.h" #include "World.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" #include "General.h" #include "Pools.h" #include "Darkel.h" diff --git a/src/peds/PlayerPed.h b/src/peds/PlayerPed.h index b27cd983..c139bbbc 100644 --- a/src/peds/PlayerPed.h +++ b/src/peds/PlayerPed.h @@ -1,8 +1,10 @@ #pragma once #include "Ped.h" -#include "Wanted.h" -#include "Pad.h" + +class CPad; +class CCopPed; +class CWanted; class CPlayerPed : public CPed { diff --git a/src/peds/Population.cpp b/src/peds/Population.cpp index 6b674dd3..3bf81066 100644 --- a/src/peds/Population.cpp +++ b/src/peds/Population.cpp @@ -4,6 +4,8 @@ #include "General.h" #include "World.h" #include "Population.h" +#include "CopPed.h" +#include "Wanted.h" #include "FileMgr.h" #include "Gangs.h" #include "ModelIndices.h" @@ -11,6 +13,7 @@ #include "CivilianPed.h" #include "EmergencyPed.h" #include "Replay.h" +#include "Camera.h" #include "CutsceneMgr.h" #include "CarCtrl.h" #include "IniFile.h" @@ -110,7 +113,8 @@ CPopulation::ChooseCivilianOccupation(int32 group) return ms_pPedGroups[group].models[CGeneral::GetRandomNumberInRange(0, NUMMODELSPERPEDGROUP)]; } -eCopType +// returns eCopType +int32 CPopulation::ChoosePolicePedOccupation() { CGeneral::GetRandomNumber(); @@ -512,9 +516,9 @@ CPopulation::AddPed(ePedType pedType, uint32 miOrCopType, CVector const &coors) uint32 weapon; if (CGeneral::GetRandomNumberInRange(0, 100) >= 50) - weapon = ped->GiveWeapon(CGangs::GetGangInfo(pedType - PEDTYPE_GANG1)->m_Weapon2, 25001); + weapon = ped->GiveWeapon((eWeaponType)CGangs::GetGangInfo(pedType - PEDTYPE_GANG1)->m_Weapon2, 25001); else - weapon = ped->GiveWeapon(CGangs::GetGangInfo(pedType - PEDTYPE_GANG1)->m_Weapon1, 25001); + weapon = ped->GiveWeapon((eWeaponType)CGangs::GetGangInfo(pedType - PEDTYPE_GANG1)->m_Weapon1, 25001); ped->SetCurrentWeapon(weapon); return ped; } diff --git a/src/peds/Population.h b/src/peds/Population.h index b299c0a1..f9e6c3b7 100644 --- a/src/peds/Population.h +++ b/src/peds/Population.h @@ -2,11 +2,11 @@ #include "Game.h" #include "PedType.h" -#include "CopPed.h" class CPed; class CVehicle; class CDummyObject; +class CObject; struct PedGroup { @@ -71,7 +71,7 @@ public: static bool IsPointInSafeZone(CVector *coors); static void RemovePed(CPed *ent); static int32 ChooseCivilianOccupation(int32); - static eCopType ChoosePolicePedOccupation(); + static int32 ChoosePolicePedOccupation(); static int32 ChooseGangOccupation(int); static void FindCollisionZoneForCoors(CVector*, int*, eLevelName*); static void FindClosestZoneForCoors(CVector*, int*, eLevelName, eLevelName); diff --git a/src/render/Hud.cpp b/src/render/Hud.cpp index 52930067..51aa390f 100644 --- a/src/render/Hud.cpp +++ b/src/render/Hud.cpp @@ -11,6 +11,7 @@ #include "Pad.h" #include "Radar.h" #include "Replay.h" +#include "Wanted.h" #include "Sprite.h" #include "Sprite2d.h" #include "Text.h" diff --git a/src/vehicles/Automobile.cpp b/src/vehicles/Automobile.cpp index 44ff6b6d..8cb0cfa4 100644 --- a/src/vehicles/Automobile.cpp +++ b/src/vehicles/Automobile.cpp @@ -39,6 +39,7 @@ #include "PathFind.h" #include "AnimManager.h" #include "RpAnimBlend.h" +#include "AnimBlendAssociation.h" #include "Ped.h" #include "PlayerPed.h" #include "Object.h" diff --git a/src/vehicles/Heli.cpp b/src/vehicles/Heli.cpp index 9fc50651..3dc1deeb 100644 --- a/src/vehicles/Heli.cpp +++ b/src/vehicles/Heli.cpp @@ -19,6 +19,8 @@ #include "World.h" #include "WaterLevel.h" #include "PlayerPed.h" +#include "Wanted.h" +#include "DMAudio.h" #include "Object.h" #include "HandlingMgr.h" #include "Heli.h" diff --git a/src/vehicles/Plane.cpp b/src/vehicles/Plane.cpp index e44ff996..c2b9e493 100644 --- a/src/vehicles/Plane.cpp +++ b/src/vehicles/Plane.cpp @@ -7,6 +7,8 @@ #include "Streaming.h" #include "Replay.h" #include "Camera.h" +#include "DMAudio.h" +#include "Wanted.h" #include "Coronas.h" #include "Particle.h" #include "Explosion.h" diff --git a/src/vehicles/Train.cpp b/src/vehicles/Train.cpp index 6446e6d1..7d81fd57 100644 --- a/src/vehicles/Train.cpp +++ b/src/vehicles/Train.cpp @@ -10,6 +10,7 @@ #include "Coronas.h" #include "World.h" #include "Ped.h" +#include "DMAudio.h" #include "HandlingMgr.h" #include "Train.h" diff --git a/src/vehicles/Vehicle.cpp b/src/vehicles/Vehicle.cpp index ac0da52c..54bc2c01 100644 --- a/src/vehicles/Vehicle.cpp +++ b/src/vehicles/Vehicle.cpp @@ -16,6 +16,7 @@ #include "Renderer.h" #include "DMAudio.h" #include "Radar.h" +#include "Fire.h" bool &CVehicle::bWheelsOnlyCheat = *(bool *)0x95CD78; bool &CVehicle::bAllDodosCheat = *(bool *)0x95CD75; diff --git a/src/weapons/WeaponInfo.cpp b/src/weapons/WeaponInfo.cpp index 6884d347..a4a1a085 100644 --- a/src/weapons/WeaponInfo.cpp +++ b/src/weapons/WeaponInfo.cpp @@ -3,7 +3,9 @@ #include "main.h" #include "FileMgr.h" #include "WeaponInfo.h" +#include "AnimManager.h" #include "AnimBlendAssociation.h" +#include "Weapon.h" //CWeaponInfo (&CWeaponInfo::ms_apWeaponInfos)[14] = * (CWeaponInfo(*)[14]) * (uintptr*)0x6503EC; CWeaponInfo CWeaponInfo::ms_apWeaponInfos[WEAPONTYPE_TOTALWEAPONS]; diff --git a/src/weapons/WeaponInfo.h b/src/weapons/WeaponInfo.h index faa8bf7b..e2e71d23 100644 --- a/src/weapons/WeaponInfo.h +++ b/src/weapons/WeaponInfo.h @@ -1,7 +1,8 @@ #pragma once -#include "common.h" -#include "Weapon.h" -#include "AnimManager.h" + +enum AnimationId; +enum eWeaponFire; +enum eWeaponType; class CWeaponInfo { // static CWeaponInfo(&ms_apWeaponInfos)[14]; From 775bc3e666a1a61d156b5f13ec94b2ad63b8b6e5 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sat, 28 Mar 2020 20:52:25 +0300 Subject: [PATCH 35/70] garage update part 2 --- src/audio/AudioManager.cpp | 2 +- src/audio/DMAudio.h | 2 +- src/control/Garages.cpp | 785 ++++++++++++++++++++++++----- src/control/Garages.h | 30 +- src/core/Stats.cpp | 1 + src/core/Stats.h | 1 + src/modelinfo/VehicleModelInfo.cpp | 2 + src/modelinfo/VehicleModelInfo.h | 1 + 8 files changed, 681 insertions(+), 143 deletions(-) diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 39c03ef6..2565a269 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -3732,7 +3732,7 @@ cAudioManager::ProcessFrontEnd() break; case SOUND_GARAGE_NO_MONEY: case SOUND_GARAGE_BAD_VEHICLE: - case SOUND_3C: + case SOUND_GARAGE_BOMB_ALREADY_SET: m_sQueueSample.m_nSampleIndex = SFX_PICKUP_ERROR_LEFT; stereo = true; break; diff --git a/src/audio/DMAudio.h b/src/audio/DMAudio.h index 41901c0d..90fe96b5 100644 --- a/src/audio/DMAudio.h +++ b/src/audio/DMAudio.h @@ -65,7 +65,7 @@ enum eSound : int16 SOUND_GARAGE_NO_MONEY = 57, SOUND_GARAGE_BAD_VEHICLE = 58, SOUND_GARAGE_OPENING = 59, - SOUND_GARAGE_DENIED = 60, + SOUND_GARAGE_BOMB_ALREADY_SET = 60, SOUND_GARAGE_BOMB1_SET = 61, SOUND_GARAGE_BOMB2_SET = 62, SOUND_GARAGE_BOMB3_SET = 63, diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index af443f8e..27392591 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -5,6 +5,7 @@ #include "General.h" #include "Font.h" +#include "HandlingMgr.h" #include "Hud.h" #include "Messages.h" #include "ModelIndices.h" @@ -28,28 +29,62 @@ #define ROTATED_DOOR_CLOSE_SPEED (0.02f) #define DEFAULT_DOOR_OPEN_SPEED (0.035f) #define DEFAULT_DOOR_CLOSE_SPEED (0.04f) +#define CRUSHER_CRANE_SPEED 0.005f +// Prices #define BOMB_PRICE 1000 #define RESPRAY_PRICE 1000 +// Distances #define DISTANCE_TO_CALL_OFF_CHASE 10.0f #define DISTANCE_FOR_MRWHOOP_HACK 4.0f #define DISTANCE_TO_ACTIVATE_GARAGE 8.0f +#define DISTANCE_TO_ACTIVATE_KEEPCAR_GARAGE 17.0f #define DISTANCE_TO_CLOSE_MISSION_GARAGE 30.0f -#define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE 25.0 +#define DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE 25.0 +#define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE 40.0f +#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT 2.4f +#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR 15.0f +#define DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE 70.0f +#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT 1.7f +#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_IN_CAR 10.0f +#define DISTANCE_TO_SHOW_HIDEOUT_MESSAGE 5.0f +// Time #define TIME_TO_RESPRAY 2000 +#define TIME_TO_SETUP_BOMB 2000 +#define TIME_TO_CRUSH_CAR 3000 +#define TIME_TO_PROCESS_KEEPCAR_GARAGE 2000 +// Respray stuff #define FREE_RESPRAY_HEALTH_THRESHOLD 970.0f #define NUM_PARTICLES_IN_RESPRAY 200 +// Bomb stuff #define KGS_OF_EXPLOSIVES_IN_BOMB 10 +// Collect specific cars stuff #define REWARD_FOR_FIRST_POLICE_CAR 5000 #define REWARD_FOR_FIRST_BANK_VAN 5000 #define MAX_POLICE_CARS_TO_COLLECT 10 #define MAX_BANK_VANS_TO_COLLECT 10 +// Collect cars stuff +#define MAX_SPEED_TO_SHOW_COLLECTED_MESSAGE 0.03f + +// Crusher stuff +#define CRUSHER_VEHICLE_TEST_SPAN 8 +#define CRUSHER_MIN_REWARD 25 +#define CRUSHER_MAX_REWARD 125 +#define CRUSHER_REWARD_COEFFICIENT 1.0f/500000 + +// Hideout stuff +#define MAX_STORED_CARS_IN_INDUSTRIAL 1 +#define MAX_STORED_CARS_IN_COMMERCIAL NUM_GARAGE_STORED_CARS +#define MAX_STORED_CARS_IN_SUBURBAN NUM_GARAGE_STORED_CARS +#define HIDEOUT_DOOR_SPEED_COEFFICIENT 1.7f +#define TIME_BETWEEN_HIDEOUT_MESSAGES 18000 + int32 &CGarages::BankVansCollected = *(int32 *)0x8F1B34; bool &CGarages::BombsAreFree = *(bool *)0x95CD7A; bool &CGarages::RespraysAreFree = *(bool *)0x95CD1D; @@ -268,7 +303,7 @@ void CGarage::Update() switch (m_eGarageState) { case GS_OPENED: if (IsStaticPlayerCarEntirelyInside() && !IsAnyOtherCarTouchingGarage(FindPlayerVehicle())) { - if (IsCarSprayable()) { + if (CGarages::IsCarSprayable(FindPlayerVehicle())) { if (CWorld::Players[CWorld::PlayerInFocus].m_nMoney >= RESPRAY_PRICE || CGarages::RespraysAreFree) { m_eGarageState = GS_CLOSING; CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); @@ -405,6 +440,7 @@ void CGarage::Update() //case GS_CLOSEDCONTAINSCAR: //case GS_AFTERDROPOFF: default: + break; } break; case GARAGE_BOMBSHOP1: @@ -420,7 +456,7 @@ void CGarage::Update() #endif CGarages::TriggerMessage("GA_5", -1, 4000, -1); //"Your car is already fitted with a bomb" m_eGarageState = GS_OPENEDCONTAINSCAR; - DMAudio.PlayFrontEndSound(SOUND_GARAGE_DENIED, 1); + DMAudio.PlayFrontEndSound(SOUND_GARAGE_BOMB_ALREADY_SET, 1); break; } if (!CGarages::BombsAreFree && CWorld::Players[CWorld::PlayerInFocus].m_nMoney >= BOMB_PRICE) { @@ -438,6 +474,7 @@ void CGarage::Update() m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; + m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_SETUP_BOMB; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); } UpdateDoorsHeight(); @@ -585,8 +622,8 @@ void CGarage::Update() m_eGarageState = GS_CLOSING; } } - else if (Abs(FindPlayerCoors().x - GetGarageCenterX()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE || - Abs(FindPlayerCoors().y - GetGarageCenterY()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE) { + else if (Abs(FindPlayerCoors().x - GetGarageCenterX()) > DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE || + Abs(FindPlayerCoors().y - GetGarageCenterY()) > DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE) { m_eGarageState = GS_CLOSING; m_pTarget = nil; } @@ -595,6 +632,7 @@ void CGarage::Update() case GS_CLOSING: m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); if (m_pTarget) { DestroyVehicleAndDriverAndPassengers(m_pTarget); @@ -644,166 +682,472 @@ void CGarage::Update() m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); } + UpdateDoorsHeight(); break; //case GS_OPENEDCONTAINSCAR: //case GS_CLOSEDCONTAINSCAR: //case GS_AFTERDROPOFF: default: + break; } break; - case GARAGE_COLLECTORSITEMS: case GARAGE_COLLECTCARS_1: case GARAGE_COLLECTCARS_2: case GARAGE_COLLECTCARS_3: - case GARAGE_FORCARTOCOMEOUTOF: - case GARAGE_60SECONDS: - case GARAGE_CRUSHER: - case GARAGE_MISSION_KEEPCAR: - case GARAGE_FOR_SCRIPT_TO_OPEN: - case GARAGE_HIDEOUT_ONE: - case GARAGE_HIDEOUT_TWO: - case GARAGE_HIDEOUT_THREE: - case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: - case GARAGE_KEEPS_OPENING_FOR_SPECIFIC_CAR: - case GARAGE_MISSION_KEEPCAR_REMAINCLOSED: switch (m_eGarageState) { - case GS_FULLYCLOSED: - case GS_OPENING: case GS_OPENED: + if (FindPlayerVehicle() && DoesCraigNeedThisCar(FindPlayerVehicle()->GetModelIndex())) { + m_pTarget = FindPlayerVehicle(); + m_pTarget->RegisterReference((CEntity**)&m_pTarget); + } + if (Abs(FindPlayerCoors().x - GetGarageCenterX()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE || + Abs(FindPlayerCoors().y - GetGarageCenterY()) > DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE) { + m_eGarageState = GS_CLOSING; + m_pTarget = nil; + break; + } + if (m_pTarget && !FindPlayerVehicle() && IsEntityEntirelyInside3D(m_pTarget, 0.0f) && + !IsAnyOtherCarTouchingGarage(m_pTarget) && IsEntityEntirelyOutside(FindPlayerPed(), 2.0f)) { +#ifdef FIX_BUGS + if (!m_pTarget->IsCar() || + ((CAutomobile*)(m_pTarget))->Damage.GetEngineStatus() <= ENGINE_STATUS_ON_FIRE && + ((CAutomobile*)(m_pTarget))->m_fFireBlowUpTimer == 0.0f) { +#else + if (((CAutomobile*)(m_pTarget))->Damage.GetEngineStatus() <= ENGINE_STATUS_ON_FIRE && + ((CAutomobile*)(m_pTarget))->m_fFireBlowUpTimer == 0.0f) { +#endif + if (m_pTarget->m_status != STATUS_WRECKED) { + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + m_eGarageState = GS_CLOSING; + TheCamera.SetCameraDirectlyBehindForFollowPed_CamOnAString(); + } + } + } + break; case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + if (m_pTarget) { + MarkThisCarAsCollectedForCraig(m_pTarget->GetModelIndex()); + DestroyVehicleAndDriverAndPassengers(m_pTarget); + m_pTarget = nil; + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; + } + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (FindPlayerVehicle() && + CalcSmallestDistToGarageDoorSquared( + FindPlayerVehicle()->GetPosition().x, + FindPlayerVehicle()->GetPosition().y + ) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) { + if (DoesCraigNeedThisCar(FindPlayerVehicle()->GetModelIndex())) { + if (FindPlayerVehicle()->VehicleCreatedBy == MISSION_VEHICLE) + CGarages::TriggerMessage("GA_1A", -1, 5000, -1); // Come back when you're not so busy... + else + m_eGarageState = GS_OPENING; + } + else { + if (HasCraigCollectedThisCar(FindPlayerVehicle()->GetModelIndex())) + CGarages::TriggerMessage("GA_20", -1, 5000, -1); // We got more of these than we can shift. Sorry man, no deal. + else if (FindPlayerSpeed().Magnitude() < MAX_SPEED_TO_SHOW_COLLECTED_MESSAGE) + CGarages::TriggerMessage("GA_19", -1, 5000, -1); // We're not interested in that model. + } + } + m_pTarget = nil; + break; + case GS_OPENING: + if (FindPlayerVehicle() && DoesCraigNeedThisCar(FindPlayerVehicle()->GetModelIndex())) { + m_pTarget = FindPlayerVehicle(); + m_pTarget->RegisterReference((CEntity**)&m_pTarget); + } + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_FORCARTOCOMEOUTOF: + switch (m_eGarageState) { + case GS_OPENED: + if (IsGarageEmpty()) + m_eGarageState = GS_CLOSING; + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + } + if (!IsGarageEmpty()) + m_eGarageState = GS_OPENING; + break; + case GS_FULLYCLOSED: + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; case GS_OPENEDCONTAINSCAR: case GS_CLOSEDCONTAINSCAR: case GS_AFTERDROPOFF: default: + break; } break; + case GARAGE_CRUSHER: + switch (m_eGarageState) { + case GS_OPENED: + { + int i = CPools::GetVehiclePool()->GetSize() * (CTimer::GetFrameCounter() % CRUSHER_VEHICLE_TEST_SPAN) / CRUSHER_VEHICLE_TEST_SPAN; + int end = CPools::GetVehiclePool()->GetSize() * (CTimer::GetFrameCounter() % CRUSHER_VEHICLE_TEST_SPAN + 1) / CRUSHER_VEHICLE_TEST_SPAN; + for (; i < end; i++) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle) + continue; + if (pVehicle->IsCar() && IsEntityEntirelyInside3D(pVehicle, 0.0f)) { + m_eGarageState = GS_CLOSING; + m_pTarget = pVehicle; + m_pTarget->RegisterReference((CEntity**)&m_pTarget); + } + } + break; + } + case GS_CLOSING: + if (m_pTarget) { + m_fDoorPos = max(0.0f, m_fDoorPos - CRUSHER_CRANE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos < TWOPI/5) { + m_pTarget->bUsesCollision = false; + m_pTarget->bAffectedByGravity = false; + m_pTarget->SetMoveSpeed(0.0f, 0.0f, 0.0f); + } + else { + m_pTarget->SetMoveSpeed(m_pTarget->GetMoveSpeed()* Pow(0.8f, CTimer::GetTimeStep())); + } + if (m_fDoorPos == 0.0f) { + CGarages::CrushedCarId = CPools::GetVehiclePool()->GetIndex(m_pTarget); + float reward = min(CRUSHER_MAX_REWARD, CRUSHER_MIN_REWARD + m_pTarget->pHandling->nMonetaryValue * m_pTarget->m_fHealth * CRUSHER_REWARD_COEFFICIENT); + CWorld::Players[CWorld::PlayerInFocus].m_nMoney += reward; + DestroyVehicleAndDriverAndPassengers(m_pTarget); + ++CStats::CarsCrushed; + m_pTarget = nil; + m_eGarageState = GS_AFTERDROPOFF; + m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_CRUSH_CAR; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + } + } + else + m_eGarageState = GS_OPENING; + UpdateCrusherAngle(); + break; + case GS_AFTERDROPOFF: + if (CTimer::GetTimeInMilliseconds() <= m_nTimeToStartAction) { + UpdateCrusherShake((myrand() & 0xFF - 128) * 0.0002f, (myrand() & 0xFF - 128) * 0.0002f); + } + else { + UpdateCrusherShake(0.0f, 0.0f); + m_eGarageState = GS_OPENING; + } + break; + case GS_OPENING: + m_fDoorPos = min(HALFPI, m_fDoorPos + CTimer::GetTimeStep() * CRUSHER_CRANE_SPEED); + if (m_fDoorPos == HALFPI) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateCrusherAngle(); + break; + //case GS_FULLYCLOSED: + //case GS_CLOSEDCONTAINSCAR: + //case GS_OPENEDCONTAINSCAR: + + default: + break; + } + if (!FindPlayerVehicle() && (CTimer::GetFrameCounter() & 0x1F) == 0x17 && IsEntityEntirelyInside(FindPlayerPed())) + FindPlayerPed()->InflictDamage(nil, WEAPONTYPE_RAMMEDBYCAR, 300.0f, PEDPIECE_TORSO, 0); + break; + case GARAGE_MISSION_KEEPCAR: + case GARAGE_MISSION_KEEPCAR_REMAINCLOSED: + switch (m_eGarageState) { + case GS_OPENED: + if (((CVector2D)FindPlayerCoors() - CVector2D(GetGarageCenterX(), GetGarageCenterY())).MagnitudeSqr() > SQR(DISTANCE_TO_CLOSE_MISSION_GARAGE) && + !IsAnyOtherCarTouchingGarage(nil)) { + m_eGarageState = GS_CLOSING; + m_bClosingWithoutTargetCar = true; + } + else if (m_pTarget && m_pTarget == FindPlayerVehicle() && IsStaticPlayerCarEntirelyInside() && !IsAnyCarBlockingDoor()) { + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + m_eGarageState = GS_CLOSING; + m_bClosingWithoutTargetCar = false; + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + if (m_bClosingWithoutTargetCar) + m_eGarageState = GS_FULLYCLOSED; + else { + if (m_pTarget) { + m_eGarageState = GS_CLOSEDCONTAINSCAR; + m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_PROCESS_KEEPCAR_GARAGE; + m_pTarget = nil; + } + else + m_eGarageState = GS_FULLYCLOSED; + CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; + } + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (FindPlayerVehicle() == m_pTarget && m_pTarget && + CalcDistToGarageRectangleSquared( + FindPlayerVehicle()->GetPosition().x, + FindPlayerVehicle()->GetPosition().y + ) < SQR(DISTANCE_TO_ACTIVATE_KEEPCAR_GARAGE)) + m_eGarageState = GS_OPENING; + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_CLOSEDCONTAINSCAR: + if (m_eGarageType == GARAGE_MISSION_KEEPCAR && CTimer::GetTimeInMilliseconds() > m_nTimeToStartAction) + m_eGarageState = GS_OPENING; + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_FOR_SCRIPT_TO_OPEN: + switch (m_eGarageState) { + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENED: + //case GS_CLOSING: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: + switch (m_eGarageState) { + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENED: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + + case GARAGE_HIDEOUT_ONE: + case GARAGE_HIDEOUT_TWO: + case GARAGE_HIDEOUT_THREE: + switch (m_eGarageState) { + case GS_OPENED: + { + float distance = CalcDistToGarageRectangleSquared(FindPlayerCoors().x, FindPlayerCoors().y); + // Close car doors either if player is far, or if he is in vehicle and garage is full, + // or if player is very very far so that we can remove whatever is blocking garage door without him noticing + if ((distance > SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR) || + !FindPlayerVehicle() && distance > SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT) && + !IsAnyCarBlockingDoor())) + m_eGarageState = GS_CLOSING; + else if (FindPlayerVehicle() && + CountCarsWithCenterPointWithinGarage(FindPlayerVehicle()) >= + CGarages::FindMaxNumStoredCarsForGarage(m_eGarageType)) { + m_eGarageState = GS_CLOSING; + } + else if (distance > SQR(DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE)) { + m_eGarageState = GS_CLOSING; + RemoveCarsBlockingDoorNotInside(); + } + break; + } + case GS_CLOSING: +#ifndef FIX_BUGS // TODO: check and replace with ifdef + if (!IsPlayerOutsideGarage()) + m_eGarageState = GS_OPENING; + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); +#else + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (!IsPlayerOutsideGarage()) + m_eGarageState = GS_OPENING; +#endif + else if (m_fDoorPos == 0.0f) { + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + m_eGarageState = GS_FULLYCLOSED; + switch (m_eGarageType) { + case GARAGE_HIDEOUT_ONE: StoreAndRemoveCarsForThisHideout(CGarages::aCarsInSafeHouse1, MAX_STORED_CARS_IN_INDUSTRIAL); break; + case GARAGE_HIDEOUT_TWO: StoreAndRemoveCarsForThisHideout(CGarages::aCarsInSafeHouse2, MAX_STORED_CARS_IN_COMMERCIAL); break; + case GARAGE_HIDEOUT_THREE: StoreAndRemoveCarsForThisHideout(CGarages::aCarsInSafeHouse3, MAX_STORED_CARS_IN_SUBURBAN); break; + } + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + { + float distance = CalcDistToGarageRectangleSquared(FindPlayerCoors().x, FindPlayerCoors().y); + if (distance < SQR(DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT) || + distance < SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR) && FindPlayerVehicle()) { + if (FindPlayerVehicle() && CGarages::CountCarsInHideoutGarage(m_eGarageType) >= CGarages::FindMaxNumStoredCarsForGarage(m_eGarageType)) { + if (m_pDoor1) { + if (((CVector2D)FindPlayerVehicle()->GetPosition() - (CVector2D)m_pDoor1->GetPosition()).MagnitudeSqr() < SQR(DISTANCE_TO_SHOW_HIDEOUT_MESSAGE) && + CTimer::GetTimeInMilliseconds() - CGarages::LastTimeHelpMessage > TIME_BETWEEN_HIDEOUT_MESSAGES) { + CHud::SetHelpMessage(TheText.Get("GA_21"), false); // You cannot store any more cars in this garage. + CGarages::LastTimeHelpMessage = CTimer::GetTimeInMilliseconds(); + } + } + } + else { +#ifdef FIX_BUGS + bool bCreatedAllCars = false; +#else + bool bCraetedAllCars; +#endif + switch (m_eGarageType) { + case GARAGE_HIDEOUT_ONE: bCreatedAllCars = RestoreCarsForThisHideout(CGarages::aCarsInSafeHouse1); break; + case GARAGE_HIDEOUT_TWO: bCreatedAllCars = RestoreCarsForThisHideout(CGarages::aCarsInSafeHouse2); break; + case GARAGE_HIDEOUT_THREE: bCreatedAllCars = RestoreCarsForThisHideout(CGarages::aCarsInSafeHouse3); break; + } + if (bCreatedAllCars) + m_eGarageState = GS_OPENING; + } + } + break; + } + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + case GARAGE_KEEPS_OPENING_FOR_SPECIFIC_CAR: + switch (m_eGarageState) { + case GS_OPENED: + if (((CVector2D)FindPlayerCoors() - CVector2D(GetGarageCenterX(), GetGarageCenterY())).MagnitudeSqr() > SQR(DISTANCE_TO_CLOSE_MISSION_GARAGE)) { + if (m_pTarget && IsEntityEntirelyOutside(m_pTarget, 0.0f) && !IsAnyOtherCarTouchingGarage(nil)) { + m_eGarageState = GS_CLOSING; + m_bClosingWithoutTargetCar = true; + } + } + break; + case GS_CLOSING: + m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == 0.0f) { + m_eGarageState = GS_FULLYCLOSED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); + } + UpdateDoorsHeight(); + break; + case GS_FULLYCLOSED: + if (FindPlayerVehicle() == m_pTarget && m_pTarget && + CalcDistToGarageRectangleSquared( + FindPlayerVehicle()->GetPosition().x, + FindPlayerVehicle()->GetPosition().y + ) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) + m_eGarageState = GS_OPENING; + break; + case GS_OPENING: + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + if (m_fDoorPos == m_fDoorHeight) { + m_eGarageState = GS_OPENED; + DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); + } + UpdateDoorsHeight(); + break; + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: + default: + break; + } + break; + //case GARAGE_COLLECTORSITEMS: + //case GARAGE_60SECONDS: default: break; } } -WRAPPER void CGarages::Load(uint8* buf, uint32 size) { EAXJMP(0x428940); } -WRAPPER void CGarages::Save(uint8* buf, uint32 *size) { EAXJMP(0x4284E0); } +WRAPPER bool CGarage::IsStaticPlayerCarEntirelyInside() { EAXJMP(0x4251C0); } +WRAPPER bool CGarage::IsEntityEntirelyInside(CEntity*) { EAXJMP(0x425370); } +WRAPPER bool CGarage::IsEntityEntirelyInside3D(CEntity*, float) { EAXJMP(0x4254F0); } +WRAPPER bool CGarage::IsEntityEntirelyOutside(CEntity*, float) { EAXJMP(0x425740); } +WRAPPER bool CGarage::IsGarageEmpty() { EAXJMP(0x425890); } +WRAPPER bool CGarage::IsPlayerOutsideGarage() { EAXJMP(0x425910); } +WRAPPER bool CGarage::IsEntityTouching3D(CEntity*) { EAXJMP(0x425950); } +WRAPPER bool CGarage::EntityHasASphereWayOutsideGarage(CEntity*, float) { EAXJMP(0x425B30); } +WRAPPER bool CGarage::IsAnyOtherCarTouchingGarage(CVehicle* pException) { EAXJMP(0x425C90); } +WRAPPER bool CGarage::IsAnyOtherPedTouchingGarage(CPed* pException) { EAXJMP(0x425E20); } +WRAPPER bool CGarage::IsAnyCarBlockingDoor() { EAXJMP(0x425FB0); } +WRAPPER int32 CGarage::CountCarsWithCenterPointWithinGarage(CEntity* pException) { EAXJMP(0x426130); } +WRAPPER void CGarage::RemoveCarsBlockingDoorNotInside() { EAXJMP(0x4261F0); } -WRAPPER void CGarage::TidyUpGarageClose() { EAXJMP(0x427D90); } -WRAPPER void CGarage::TidyUpGarage() { EAXJMP(0x427C30); } -WRAPPER void CGarage::RefreshDoorPointers(bool) { EAXJMP(0x426980); } -WRAPPER void CGarage::UpdateCrusherAngle() { EAXJMP(0x4268A0); } -WRAPPER void CGarage::UpdateDoorsHeight() { EAXJMP(0x426730); } -WRAPPER float CGarages::FindDoorHeightForMI(int32) { EAXJMP(0x427C10); } - -bool -CGarages::IsModelIndexADoor(uint32 id) -{ - return id == MI_GARAGEDOOR1 || - id == MI_GARAGEDOOR2 || - id == MI_GARAGEDOOR3 || - id == MI_GARAGEDOOR4 || - id == MI_GARAGEDOOR5 || - id == MI_GARAGEDOOR6 || - id == MI_GARAGEDOOR7 || - id == MI_GARAGEDOOR9 || - id == MI_GARAGEDOOR10 || - id == MI_GARAGEDOOR11 || - id == MI_GARAGEDOOR12 || - id == MI_GARAGEDOOR13 || - id == MI_GARAGEDOOR14 || - id == MI_GARAGEDOOR15 || - id == MI_GARAGEDOOR16 || - id == MI_GARAGEDOOR17 || - id == MI_GARAGEDOOR18 || - id == MI_GARAGEDOOR19 || - id == MI_GARAGEDOOR20 || - id == MI_GARAGEDOOR21 || - id == MI_GARAGEDOOR22 || - id == MI_GARAGEDOOR23 || - id == MI_GARAGEDOOR24 || - id == MI_GARAGEDOOR25 || - id == MI_GARAGEDOOR26 || - id == MI_GARAGEDOOR27 || - id == MI_GARAGEDOOR28 || - id == MI_GARAGEDOOR29 || - id == MI_GARAGEDOOR30 || - id == MI_GARAGEDOOR31 || - id == MI_GARAGEDOOR32 || - id == MI_CRUSHERBODY || - id == MI_CRUSHERLID; -} - -bool CGarages::HasCarBeenCrushed(int32 handle) -{ - return CrushedCarId == handle; -} - -WRAPPER void CGarages::TriggerMessage(const char *text, int16, uint16 time, int16) { EAXJMP(0x426B20); } -WRAPPER bool CGarages::IsPointWithinHideOutGarage(CVector&) { EAXJMP(0x428260); } -WRAPPER bool CGarages::IsPointWithinAnyGarage(CVector&) { EAXJMP(0x428320); } -WRAPPER void CGarages::PlayerArrestedOrDied() { EAXJMP(0x427F60); } -WRAPPER void CGarages::SetTargetCarForMissonGarage(int16, CVehicle*) { EAXJMP(0x426BD0); } -WRAPPER bool CGarages::HasCarBeenDroppedOffYet(int16) { EAXJMP(0x426C20); } -WRAPPER void CGarages::DeActivateGarage(int16) { EAXJMP(0x426C40); } -WRAPPER void CGarages::ActivateGarage(int16) { EAXJMP(0x426C60); } - -int32 CGarages::QueryCarsCollected(int16 garage) -{ - return 0; -} - -void CGarages::GivePlayerDetonator() -{ - FindPlayerPed()->GiveWeapon(WEAPONTYPE_DETONATOR, 1); - FindPlayerPed()->GetWeapon(FindPlayerPed()->GetWeaponSlot(WEAPONTYPE_DETONATOR)).m_eWeaponState = WEAPONSTATE_READY; -} - -WRAPPER bool CGarages::HasThisCarBeenCollected(int16 garage, uint8 id) { EAXJMP(0x426D50); } -WRAPPER bool CGarages::HasResprayHappened(int16 garage) { EAXJMP(0x4274F0); } -WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity* pCar) { EAXJMP(0x427570); } - -void CGarage::OpenThisGarage() -{ - if (m_eGarageState == GS_FULLYCLOSED || m_eGarageState == GS_CLOSING || m_eGarageState == GS_CLOSEDCONTAINSCAR) - m_eGarageState = GS_OPENING; -} - -bool CGarages::IsGarageOpen(int16 garage) -{ - return aGarages[garage].IsOpen(); -} - -bool CGarages::IsGarageClosed(int16 garage) -{ - return aGarages[garage].IsClosed(); -} - -void CGarage::CloseThisGarage() -{ - if (m_eGarageState == GS_OPENED || m_eGarageState == GS_OPENING) - m_eGarageState = GS_CLOSING; -} - -void CGarages::SetGarageDoorToRotate(int16 garage) -{ - if (aGarages[garage].m_bRotatedDoor) - return; - aGarages[garage].m_bRotatedDoor = true; - aGarages[garage].m_fDoorHeight /= 2.0f; - aGarages[garage].m_fDoorHeight -= 0.1f; -} - -bool CGarages::HasImportExportGarageCollectedThisCar(int16 garage, int8 car) -{ - return CarTypesCollected[GetCarsCollectedIndexForGarageType(aGarages[garage].m_eGarageType)] & (1 << car); -} - -void CGarages::SetLeaveCameraForThisGarage(int16 garage) -{ - aGarages[garage].m_bCameraFollowsPlayer = true; -} - -#if 0 -WRAPPER void CGarages::PrintMessages(void) { EAXJMP(0x426310); } -#else void CGarages::PrintMessages() { if (CTimer::GetTimeInMilliseconds() > MessageStartTime && CTimer::GetTimeInMilliseconds() < MessageEndTime) { @@ -844,7 +1188,168 @@ void CGarages::PrintMessages() } } } -#endif + +WRAPPER bool CGarages::IsCarSprayable(CVehicle*) { EAXJMP(0x426700); } +WRAPPER void CGarage::UpdateDoorsHeight() { EAXJMP(0x426730); } +WRAPPER void CGarage::BuildRotatedDoorMatrix(CEntity*, float) { EAXJMP(0x4267C0); } +WRAPPER void CGarage::UpdateCrusherAngle() { EAXJMP(0x4268A0); } +WRAPPER void CGarage::UpdateCrusherShake(float, float) { EAXJMP(0x4268E0); } +WRAPPER void CGarage::RefreshDoorPointers(bool) { EAXJMP(0x426980); } +WRAPPER void CGarages::TriggerMessage(const char* text, int16, uint16 time, int16) { EAXJMP(0x426B20); } +WRAPPER void CGarages::SetTargetCarForMissonGarage(int16, CVehicle*) { EAXJMP(0x426BD0); } +WRAPPER bool CGarages::HasCarBeenDroppedOffYet(int16) { EAXJMP(0x426C20); } +WRAPPER void CGarages::DeActivateGarage(int16) { EAXJMP(0x426C40); } +WRAPPER void CGarages::ActivateGarage(int16) { EAXJMP(0x426C60); } + +int32 CGarages::QueryCarsCollected(int16 garage) +{ + return 0; +} + +bool CGarages::HasImportExportGarageCollectedThisCar(int16 garage, int8 car) +{ + return CarTypesCollected[GetCarsCollectedIndexForGarageType(aGarages[garage].m_eGarageType)] & (1 << car); +} + +bool CGarages::IsGarageOpen(int16 garage) +{ + return aGarages[garage].IsOpen(); +} + +bool CGarages::IsGarageClosed(int16 garage) +{ + return aGarages[garage].IsClosed(); +} + +WRAPPER bool CGarages::HasThisCarBeenCollected(int16 garage, uint8 id) { EAXJMP(0x426D50); } +WRAPPER bool CGarage::DoesCraigNeedThisCar(int32) { EAXJMP(0x426D90); } +WRAPPER bool CGarage::HasCraigCollectedThisCar(int32) { EAXJMP(0x426DF0); } +WRAPPER void CGarage::MarkThisCarAsCollectedForCraig(int32) { EAXJMP(0x426E50); } + +void CGarage::OpenThisGarage() +{ + if (m_eGarageState == GS_FULLYCLOSED || m_eGarageState == GS_CLOSING || m_eGarageState == GS_CLOSEDCONTAINSCAR) + m_eGarageState = GS_OPENING; +} + +void CGarage::CloseThisGarage() +{ + if (m_eGarageState == GS_OPENED || m_eGarageState == GS_OPENING) + m_eGarageState = GS_CLOSING; +} + +WRAPPER float CGarage::CalcDistToGarageRectangleSquared(float, float) { EAXJMP(0x426F50); } +WRAPPER float CGarage::CalcSmallestDistToGarageDoorSquared(float, float) { EAXJMP(0x426FE0); } +WRAPPER void CGarage::FindDoorsEntities() { EAXJMP(0x427060); } +WRAPPER void CGarage::FindDoorsEntitiesSectorList(CPtrList&, bool) { EAXJMP(0x427300); } +WRAPPER bool CGarages::HasResprayHappened(int16 garage) { EAXJMP(0x4274F0); } + +void CGarages::SetGarageDoorToRotate(int16 garage) +{ + if (aGarages[garage].m_bRotatedDoor) + return; + aGarages[garage].m_bRotatedDoor = true; + aGarages[garage].m_fDoorHeight /= 2.0f; + aGarages[garage].m_fDoorHeight -= 0.1f; +} + +void CGarages::SetLeaveCameraForThisGarage(int16 garage) +{ + aGarages[garage].m_bCameraFollowsPlayer = true; +} + +WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity* pCar) { EAXJMP(0x427570); } + +bool CGarages::HasCarBeenCrushed(int32 handle) +{ + return CrushedCarId == handle; +} + +WRAPPER void CStoredCar::StoreCar(CVehicle*) { EAXJMP(0x4275C0); } +WRAPPER CVehicle* CStoredCar::RestoreCar() { EAXJMP(0x427690); } +WRAPPER void CGarage::StoreAndRemoveCarsForThisHideout(CStoredCar*, int32) { EAXJMP(0x427840); } +WRAPPER bool CGarage::RestoreCarsForThisHideout(CStoredCar*) { EAXJMP(0x427A40); } +WRAPPER bool CGarages::IsPointInAGarageCameraZone(CVector) { EAXJMP(0x427AB0); } +WRAPPER bool CGarages::CameraShouldBeOutside() { EAXJMP(0x427BC0); } + +void CGarages::GivePlayerDetonator() +{ + FindPlayerPed()->GiveWeapon(WEAPONTYPE_DETONATOR, 1); + FindPlayerPed()->GetWeapon(FindPlayerPed()->GetWeaponSlot(WEAPONTYPE_DETONATOR)).m_eWeaponState = WEAPONSTATE_READY; +} + +WRAPPER float CGarages::FindDoorHeightForMI(int32) { EAXJMP(0x427C10); } +WRAPPER void CGarage::TidyUpGarage() { EAXJMP(0x427C30); } +WRAPPER void CGarage::TidyUpGarageClose() { EAXJMP(0x427D90); } +WRAPPER void CGarages::PlayerArrestedOrDied() { EAXJMP(0x427F60); } +WRAPPER void CGarage::PlayerArrestedOrDied() { EAXJMP(0x427FC0); } +WRAPPER void CGarage::CenterCarInGarage(CVehicle*) { EAXJMP(0x428000); } +WRAPPER void CGarages::CloseHideOutGaragesBeforeSave() { EAXJMP(0x428130); } +WRAPPER int32 CGarages::CountCarsInHideoutGarage(eGarageType) { EAXJMP(0x4281E0); } +WRAPPER int32 CGarages::FindMaxNumStoredCarsForGarage(eGarageType) { EAXJMP(0x428230); } +WRAPPER bool CGarages::IsPointWithinHideOutGarage(CVector&) { EAXJMP(0x428260); } +WRAPPER bool CGarages::IsPointWithinAnyGarage(CVector&) { EAXJMP(0x428320); } +WRAPPER void CGarages::SetAllDoorsBackToOriginalHeight() { EAXJMP(0x4283D0); } +WRAPPER void CGarages::Save(uint8* buf, uint32* size) { EAXJMP(0x4284E0); } + +CStoredCar::CStoredCar(const CStoredCar& other) +{ + m_nModelIndex = other.m_nModelIndex; + m_vecPos = other.m_vecPos; + m_vecAngle = other.m_vecAngle; + m_bBulletproof = other.m_bBulletproof; + m_bFireproof = other.m_bFireproof; + m_bExplosionproof = other.m_bExplosionproof; + m_bCollisionproof = other.m_bCollisionproof; + m_bMeleeproof = other.m_bMeleeproof; + m_nPrimaryColor = other.m_nPrimaryColor; + m_nSecondaryColor = other.m_nSecondaryColor; + m_nRadioStation = other.m_nRadioStation; + m_nVariationA = other.m_nVariationA; + m_nVariationB = other.m_nVariationB; + m_nCarBombType = other.m_nCarBombType; +} + +WRAPPER void CGarages::Load(uint8* buf, uint32 size) { EAXJMP(0x428940); } + +bool +CGarages::IsModelIndexADoor(uint32 id) +{ + return id == MI_GARAGEDOOR1 || + id == MI_GARAGEDOOR2 || + id == MI_GARAGEDOOR3 || + id == MI_GARAGEDOOR4 || + id == MI_GARAGEDOOR5 || + id == MI_GARAGEDOOR6 || + id == MI_GARAGEDOOR7 || + id == MI_GARAGEDOOR9 || + id == MI_GARAGEDOOR10 || + id == MI_GARAGEDOOR11 || + id == MI_GARAGEDOOR12 || + id == MI_GARAGEDOOR13 || + id == MI_GARAGEDOOR14 || + id == MI_GARAGEDOOR15 || + id == MI_GARAGEDOOR16 || + id == MI_GARAGEDOOR17 || + id == MI_GARAGEDOOR18 || + id == MI_GARAGEDOOR19 || + id == MI_GARAGEDOOR20 || + id == MI_GARAGEDOOR21 || + id == MI_GARAGEDOOR22 || + id == MI_GARAGEDOOR23 || + id == MI_GARAGEDOOR24 || + id == MI_GARAGEDOOR25 || + id == MI_GARAGEDOOR26 || + id == MI_GARAGEDOOR27 || + id == MI_GARAGEDOOR28 || + id == MI_GARAGEDOOR29 || + id == MI_GARAGEDOOR30 || + id == MI_GARAGEDOOR31 || + id == MI_GARAGEDOOR32 || + id == MI_CRUSHERBODY || + id == MI_CRUSHERLID; +} + STARTPATCHES InjectHook(0x421C60, CGarages::Init, PATCH_JUMP); diff --git a/src/control/Garages.h b/src/control/Garages.h index 89d51a05..f12ccd0f 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -66,6 +66,9 @@ class CStoredCar int8 m_nCarBombType; public: void Init() { m_nModelIndex = 0; } + CStoredCar(const CStoredCar& other); + void StoreCar(CVehicle*); + CVehicle* RestoreCar(); }; static_assert(sizeof(CStoredCar) == 0x28, "CStoredCar"); @@ -139,12 +142,30 @@ public: void UpdateDoorsHeight(); bool IsEntityEntirelyInside3D(CEntity*, float); bool IsEntityEntirelyOutside(CEntity*, float); + bool IsEntityEntirelyInside(CEntity*); float CalcDistToGarageRectangleSquared(float, float); + float CalcSmallestDistToGarageDoorSquared(float, float); bool IsAnyOtherCarTouchingGarage(CVehicle* pException); bool IsStaticPlayerCarEntirelyInside(); bool IsPlayerOutsideGarage(); - bool IsCarSprayable(); + bool IsAnyCarBlockingDoor(); void CenterCarInGarage(CVehicle*); + bool DoesCraigNeedThisCar(int32); + void MarkThisCarAsCollectedForCraig(int32); + bool HasCraigCollectedThisCar(int32); + bool IsGarageEmpty(); + void UpdateCrusherShake(float, float); + int32 CountCarsWithCenterPointWithinGarage(CEntity* pException); + void RemoveCarsBlockingDoorNotInside(); + void StoreAndRemoveCarsForThisHideout(CStoredCar*, int32); + bool RestoreCarsForThisHideout(CStoredCar*); + bool IsEntityTouching3D(CEntity*); + bool EntityHasASphereWayOutsideGarage(CEntity*, float); + bool IsAnyOtherPedTouchingGarage(CPed* pException); + void BuildRotatedDoorMatrix(CEntity*, float); + void FindDoorsEntities(); + void FindDoorsEntitiesSectorList(CPtrList&, bool); + void PlayerArrestedOrDied(); }; static_assert(sizeof(CGarage) == 140, "CGarage"); @@ -207,6 +228,13 @@ public: static bool HasImportExportGarageCollectedThisCar(int16, int8); static void SetLeaveCameraForThisGarage(int16); static bool IsThisCarWithinGarageArea(int16, CEntity*); + static bool IsCarSprayable(CVehicle*); + static int32 FindMaxNumStoredCarsForGarage(eGarageType); + static int32 CountCarsInHideoutGarage(eGarageType); + static bool IsPointInAGarageCameraZone(CVector); + static bool CameraShouldBeOutside(); + static void CloseHideOutGaragesBeforeSave(); + static void SetAllDoorsBackToOriginalHeight(); static int GetBombTypeForGarageType(eGarageType type) { return type - GARAGE_BOMBSHOP1 + 1; } static int GetCarsCollectedIndexForGarageType(eGarageType type) { return type - GARAGE_COLLECTCARS_1; } diff --git a/src/core/Stats.cpp b/src/core/Stats.cpp index 2a3f06b3..9478479b 100644 --- a/src/core/Stats.cpp +++ b/src/core/Stats.cpp @@ -49,6 +49,7 @@ int32& CStats::TimeTakenDefuseMission = *(int32*)0x880E24; int32& CStats::TotalNumberKillFrenzies = *(int32*)0x8E2884; int32& CStats::TotalNumberMissions = *(int32*)0x8E2820; int32& CStats::KgOfExplosivesUsed = *(int32*)0x8F2510; +int32& CStats::CarsCrushed = *(int32*)0x943050; int32(&CStats::FastestTimes)[CStats::TOTAL_FASTEST_TIMES] = *(int32(*)[CStats::TOTAL_FASTEST_TIMES])*(uintptr*)0x6E9128; int32(&CStats::HighestScores)[CStats::TOTAL_HIGHEST_SCORES] = *(int32(*)[CStats::TOTAL_HIGHEST_SCORES]) * (uintptr*)0x8622B0; diff --git a/src/core/Stats.h b/src/core/Stats.h index f6ff8187..1d220905 100644 --- a/src/core/Stats.h +++ b/src/core/Stats.h @@ -54,6 +54,7 @@ public: static int32(&FastestTimes)[TOTAL_FASTEST_TIMES]; static int32(&HighestScores)[TOTAL_HIGHEST_SCORES]; static int32 &KgOfExplosivesUsed; + static int32 &CarsCrushed; public: static void RegisterFastestTime(int32, int32); diff --git a/src/modelinfo/VehicleModelInfo.cpp b/src/modelinfo/VehicleModelInfo.cpp index 87f01177..42ad635b 100644 --- a/src/modelinfo/VehicleModelInfo.cpp +++ b/src/modelinfo/VehicleModelInfo.cpp @@ -1113,6 +1113,8 @@ public: }; STARTPATCHES + InjectHook(0x427820, &CVehicleModelInfo::SetComponentsToUse, PATCH_JUMP); + InjectHook(0x51FDC0, &CVehicleModelInfo_::DeleteRwObject_, PATCH_JUMP); InjectHook(0x51FCB0, &CVehicleModelInfo_::CreateInstance_, PATCH_JUMP); InjectHook(0x51FC60, &CVehicleModelInfo_::SetClump_, PATCH_JUMP); diff --git a/src/modelinfo/VehicleModelInfo.h b/src/modelinfo/VehicleModelInfo.h index 1a6d6a55..5969c4ca 100644 --- a/src/modelinfo/VehicleModelInfo.h +++ b/src/modelinfo/VehicleModelInfo.h @@ -132,5 +132,6 @@ public: static void ShutdownEnvironmentMaps(void); static int GetMaximumNumberOfPassengersFromNumberOfDoors(int id); + static void SetComponentsToUse(int8 c1, int8 c2) { ms_compsToUse[0] = c1; ms_compsToUse[1] = c2; } }; static_assert(sizeof(CVehicleModelInfo) == 0x1F8, "CVehicleModelInfo: error"); From b235c358341b288f34cee5936a376f71d0cfbf9a Mon Sep 17 00:00:00 2001 From: Filip Gawin Date: Fri, 27 Mar 2020 21:50:52 +0100 Subject: [PATCH 36/70] Cleanup patching system --- src/core/patcher.cpp | 57 ++++++++++++++++++++++++++++++++++++++++++++ src/core/re3.cpp | 54 ----------------------------------------- 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/src/core/patcher.cpp b/src/core/patcher.cpp index 5fdbdf8b..19ca5f07 100644 --- a/src/core/patcher.cpp +++ b/src/core/patcher.cpp @@ -1,6 +1,11 @@ #include "common.h" #include "patcher.h" +#include +#include + +#include + StaticPatcher *StaticPatcher::ms_head; StaticPatcher::StaticPatcher(Patcher func) @@ -20,3 +25,55 @@ StaticPatcher::Apply() } ms_head = nil; } + +std::vector usedAddresses; + +static DWORD protect[2]; +static uint32 protect_address; +static uint32 protect_size; + +void +Protect_internal(uint32 address, uint32 size) +{ + protect_address = address; + protect_size = size; + VirtualProtect((void*)address, size, PAGE_EXECUTE_READWRITE, &protect[0]); +} + +void +Unprotect_internal(void) +{ + VirtualProtect((void*)protect_address, protect_size, protect[0], &protect[1]); +} + +void +InjectHook_internal(uint32 address, uint32 hook, int type) +{ + if(std::any_of(usedAddresses.begin(), usedAddresses.end(), + [address](uint32 value) { return value == address; })) { + debug("Used address %#06x twice when injecting hook\n", address); + } + + usedAddresses.push_back(address); + + + switch(type){ + case PATCH_JUMP: + VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); + *(uint8*)address = 0xE9; + break; + case PATCH_CALL: + VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); + *(uint8*)address = 0xE8; + break; + default: + VirtualProtect((void*)(address + 1), 4, PAGE_EXECUTE_READWRITE, &protect[0]); + break; + } + + *(ptrdiff_t*)(address + 1) = hook - address - 5; + if(type == PATCH_NOTHING) + VirtualProtect((void*)(address + 1), 4, protect[0], &protect[1]); + else + VirtualProtect((void*)address, 5, protect[0], &protect[1]); +} \ No newline at end of file diff --git a/src/core/re3.cpp b/src/core/re3.cpp index ffb2a7a2..a65e6d76 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -22,62 +22,8 @@ #include "Console.h" #include "Debug.h" -#include -#include #include -std::vector usedAddresses; - -static DWORD protect[2]; -static uint32 protect_address; -static uint32 protect_size; - -void -Protect_internal(uint32 address, uint32 size) -{ - protect_address = address; - protect_size = size; - VirtualProtect((void*)address, size, PAGE_EXECUTE_READWRITE, &protect[0]); -} - -void -Unprotect_internal(void) -{ - VirtualProtect((void*)protect_address, protect_size, protect[0], &protect[1]); -} - -void -InjectHook_internal(uint32 address, uint32 hook, int type) -{ - if(std::any_of(usedAddresses.begin(), usedAddresses.end(), - [address](uint32 value) { return (int32)value == address; })) { - debug("Used address %#06x twice when injecting hook\n", address); - } - - usedAddresses.push_back((int32)address); - - - switch(type){ - case PATCH_JUMP: - VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); - *(uint8*)address = 0xE9; - break; - case PATCH_CALL: - VirtualProtect((void*)address, 5, PAGE_EXECUTE_READWRITE, &protect[0]); - *(uint8*)address = 0xE8; - break; - default: - VirtualProtect((void*)((uint32)address + 1), 4, PAGE_EXECUTE_READWRITE, &protect[0]); - break; - } - - *(ptrdiff_t*)(address + 1) = (uintptr_t)hook - (uintptr_t)address - 5; - if(type == PATCH_NOTHING) - VirtualProtect((void*)(address + 1), 4, protect[0], &protect[1]); - else - VirtualProtect((void*)address, 5, protect[0], &protect[1]); -} - void **rwengine = *(void***)0x5A10E1; DebugMenuAPI gDebugMenuAPI; From 58ec4b21577c7948d51ca62c056300a6140e3290 Mon Sep 17 00:00:00 2001 From: Filip Gawin Date: Fri, 27 Mar 2020 16:45:40 +0100 Subject: [PATCH 37/70] ProcessVehicleEngine and ProcessActiveQueues Also some smaller fixes, thx erorcun for help. --- src/audio/AudioManager.cpp | 491 ++++++++++++++++++++++++++++++++++--- src/audio/AudioManager.h | 4 +- 2 files changed, 456 insertions(+), 39 deletions(-) diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 417fddf1..2be8e36a 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -68,6 +68,7 @@ const int molotovVolume = 50; const int rainOnVehicleIntensity = 22; const int reverseGearIntensity = 30; +const int engineDamageIntensity = 40; const bool hornPatternsArray[8][44] = { @@ -417,7 +418,7 @@ cAudioManager::AddReleasingSounds() } sample.field_56 = 0; } - memcpy(&m_sQueueSample, &sample, sizeof(sample)); + memcpy(&m_sQueueSample, &sample, sizeof(tSound)); AddSampleToRequestedQueue(); } } @@ -2743,11 +2744,219 @@ cAudioManager::PreTerminateGameSpecificShutdown() } } -WRAPPER void cAudioManager::ProcessActiveQueues() { - EAXJMP(0x57BA60); + bool flag; + float position2; + float position1; + + uint32 v28; + uint32 v29; + + float x; + float usedX; + float usedY; + float usedZ; + + uint8 vol; + uint8 emittingVol; + CVector position; + + for (int32 i = 0; i < m_bActiveSamples; i++) { + m_asSamples[m_bActiveSampleQueue][i].m_bIsProcessed = 0; + m_asActiveSamples[i].m_bIsProcessed = 0; + } + + for (int32 i = 0; i < m_bSampleRequestQueuesStatus[m_bActiveSampleQueue]; ++i) { + tSound& sample = m_asSamples[m_bActiveSampleQueue][m_abSampleQueueIndexTable[m_bActiveSampleQueue][i]]; + if (sample.m_nSampleIndex != NO_SAMPLE) { + for (int32 j = 0; j < m_bActiveSamples; ++j) { + if (sample.m_nEntityIndex == m_asActiveSamples[j].m_nEntityIndex && + sample.m_counter == m_asActiveSamples[j].m_counter && + sample.m_nSampleIndex == m_asActiveSamples[j].m_nSampleIndex) { + if (sample.m_nLoopCount) { + if (m_FrameCounter & 1) { + if (!(j & 1)) { flag = 0; } + flag = 1; + } + else { + if (!(j & 1)) + flag = 1; + else + flag = 0; + } + if (flag && !SampleManager.GetChannelUsedFlag(j)) { + sample.m_bLoopEnded = 1; + m_asActiveSamples[j].m_bLoopEnded = 1; + m_asActiveSamples[j].m_nSampleIndex = NO_SAMPLE; + m_asActiveSamples[j].m_nEntityIndex = -5; + continue; + } + } + sample.m_bIsProcessed = 1; + m_asActiveSamples[j].m_bIsProcessed = 1; + sample.field_88 = -1; + if (!sample.field_56) { + if (sample.m_bIsDistant) { + if (field_4) { + emittingVol = 2 * min(63, sample.m_bEmittingVolume); + } + else { + emittingVol = sample.m_bEmittingVolume; + } + SampleManager.SetChannelFrequency(j, sample.m_nFrequency); + SampleManager.SetChannelEmittingVolume(j, emittingVol); + } + else { + m_asActiveSamples[j].m_fDistance = sample.m_fDistance; + position2 = sample.m_fDistance; + position1 = m_asActiveSamples[j].m_fDistance; + sample.m_nFrequency = ComputeDopplerEffectedFrequency( + sample.m_nFrequency, position1, position2, sample.field_48); + if (sample.m_nFrequency != m_asActiveSamples[j].m_nFrequency) { + int32 freq; + if (sample.m_nFrequency <= + m_asActiveSamples[j].m_nFrequency) { + freq = max(sample.m_nFrequency, + m_asActiveSamples[j].m_nFrequency - + 6000); + } + else { + freq = min(sample.m_nFrequency, + m_asActiveSamples[j].m_nFrequency + + 6000); + } + m_asActiveSamples[j].m_nFrequency = freq; + SampleManager.SetChannelFrequency(j, freq); + } + + if (sample.m_bEmittingVolume != + m_asActiveSamples[j].m_bEmittingVolume) { + if (sample.m_bEmittingVolume <= + m_asActiveSamples[j].m_bEmittingVolume) { + vol = max( + m_asActiveSamples[j].m_bEmittingVolume - 10, + sample.m_bEmittingVolume); + } + else { + vol = min( + m_asActiveSamples[j].m_bEmittingVolume + 10, + sample.m_bEmittingVolume); + } + + uint8 emittingVol; + if (field_4) { + emittingVol = 2 * min(63, vol); + } + else { + emittingVol = vol; + } + SampleManager.SetChannelEmittingVolume(j, emittingVol); + m_asActiveSamples[j].m_bEmittingVolume = vol; + } + TranslateEntity(&sample.m_vecPos, &position); + SampleManager.SetChannel3DPosition(j, position.x, position.y, + position.z); + SampleManager.SetChannel3DDistances( + j, sample.m_fSoundIntensity, + 0.25f * sample.m_fSoundIntensity); + } + SampleManager.SetChannelReverbFlag(j, sample.m_bReverbFlag); + continue; + } + sample.m_bIsProcessed = 0; + m_asActiveSamples[j].m_bIsProcessed = 0; + break; + } + } + } + } + for (int32 i = 0; i < m_bActiveSamples; i++) { + if (m_asActiveSamples[i].m_nSampleIndex != NO_SAMPLE && !m_asActiveSamples[i].m_bIsProcessed) { + SampleManager.StopChannel(i); + m_asActiveSamples[i].m_nSampleIndex = NO_SAMPLE; + m_asActiveSamples[i].m_nEntityIndex = -5; + } + } + for (int32 i = 0; i < m_bSampleRequestQueuesStatus[m_bActiveSampleQueue]; ++i) { + + tSound& sample = m_asSamples[m_bActiveSampleQueue][m_abSampleQueueIndexTable[m_bActiveSampleQueue][i]]; + if (!sample.m_bIsProcessed && !sample.m_bLoopEnded && + m_asAudioEntities[sample.m_nEntityIndex].m_bIsUsed && sample.m_nSampleIndex < NO_SAMPLE) { + if (sample.m_counter > 255 && sample.m_nLoopCount && sample.m_bLoopsRemaining) { + --sample.m_bLoopsRemaining; + sample.field_76 = 1; + } + else { + for (int32 j = 0; j < m_bActiveSamples; ++j) { + if (!m_asActiveSamples[j].m_bIsProcessed) { + if (sample.m_nLoopCount) { + v28 = sample.m_nFrequency / field_19192; + v29 = sample.m_nLoopCount * + SampleManager.GetSampleLength(sample.m_nSampleIndex); + if (v28 == 0) continue; + sample.field_76 = v29 / v28 + 1; + } + memcpy(&m_asActiveSamples[j], &sample, sizeof(tSound)); + if (!m_asActiveSamples[j].m_bIsDistant) + TranslateEntity(&m_asActiveSamples[j].m_vecPos, &position); + if (field_4) { + emittingVol = + 2 * min(63, m_asActiveSamples[j].m_bEmittingVolume); + } + else { + emittingVol = m_asActiveSamples[j].m_bEmittingVolume; + } + if (SampleManager.InitialiseChannel(j, + m_asActiveSamples[j].m_nSampleIndex, + m_asActiveSamples[j].m_bBankIndex)) { + SampleManager.SetChannelFrequency( + j, m_asActiveSamples[j].m_nFrequency); + SampleManager.SetChannelEmittingVolume(j, emittingVol); + SampleManager.SetChannelLoopPoints( + j, m_asActiveSamples[j].m_nLoopStart, + m_asActiveSamples[j].m_nLoopEnd); + SampleManager.SetChannelLoopCount( + j, m_asActiveSamples[j].m_nLoopCount); + SampleManager.SetChannelReverbFlag( + j, m_asActiveSamples[j].m_bReverbFlag); + if (m_asActiveSamples[j].m_bIsDistant) { + uint8 offset = m_asActiveSamples[j].m_bOffset; + if (offset == 63) { + x = 0.f; + } + else if (offset >= 63) { + x = (offset - 63) * 1000.f / 63; + } + else { + x = -(63 - offset) * 1000.f / 63; + } + usedX = x; + usedY = 0.f; + usedZ = 0.f; + m_asActiveSamples[j].m_fSoundIntensity = 100000.0f; + } + else { + usedX = position.x; + usedY = position.y; + usedZ = position.z; + } + SampleManager.SetChannel3DPosition(j, usedX, usedY, usedZ); + SampleManager.SetChannel3DDistances( + j, m_asActiveSamples[j].m_fSoundIntensity, + 0.25f * m_asActiveSamples[j].m_fSoundIntensity); + SampleManager.StartChannel(j); + } + m_asActiveSamples[j].m_bIsProcessed = 1; + sample.m_bIsProcessed = 1; + sample.field_88 = -1; + break; + } + } + } + } + } } bool @@ -3057,7 +3266,7 @@ cAudioManager::ProcessBridgeMotor() m_sQueueSample.m_bVolume = ComputeVolume(maxVolume, bridgeIntensity, m_sQueueSample.m_fDistance); if(m_sQueueSample.m_bVolume) { m_sQueueSample.m_counter = 1; - m_sQueueSample.m_nSampleIndex = SFX_FISHING_BOAT_IDLE; + m_sQueueSample.m_nSampleIndex = SFX_FISHING_BOAT_IDLE; // todo check sfx name m_sQueueSample.m_bBankIndex = SAMPLEBANK_MAIN; m_sQueueSample.m_bIsDistant = false; m_sQueueSample.field_16 = 1; @@ -3128,7 +3337,7 @@ cAudioManager::ProcessBridgeWarning() m_sQueueSample.m_bVolume = ComputeVolume(100, 450.f, m_sQueueSample.m_fDistance); if(m_sQueueSample.m_bVolume) { m_sQueueSample.m_counter = 0; - m_sQueueSample.m_nSampleIndex = 457; + m_sQueueSample.m_nSampleIndex = SFX_BRIDGE_OPEN_WARNING; m_sQueueSample.m_bBankIndex = SAMPLEBANK_MAIN; m_sQueueSample.m_bIsDistant = false; m_sQueueSample.field_16 = 1; @@ -3154,7 +3363,7 @@ cAudioManager::ProcessCarBombTick(cVehicleParams *params) { CAutomobile *automobile; - if(params->m_fDistance >= 1600.f) return false; + if(params->m_fDistance >= SQR(40.f)) return false; automobile = (CAutomobile *)params->m_pVehicle; if(automobile->bEngineOn && automobile->m_bombType == CARBOMB_TIMEDACTIVE) { CalculateDistance(params->m_bDistanceCalculated, params->m_fDistance); @@ -3411,11 +3620,11 @@ cAudioManager::ProcessEngineDamage(cVehicleParams *params) uint8 engineStatus; uint8 emittingVolume; - if(params->m_fDistance >= 1600.f) return false; + if(params->m_fDistance >= SQR(engineDamageIntensity)) return false; veh = (CAutomobile *)params->m_pVehicle; if(veh->bEngineOn) { engineStatus = veh->Damage.GetEngineStatus(); - if(engineStatus > 250u || engineStatus < 100) return true; + if(engineStatus > 250 || engineStatus < 100) return true; if(engineStatus < 225) { m_sQueueSample.m_nSampleIndex = SFX_JUMBO_TAXI; emittingVolume = 6; @@ -3428,7 +3637,7 @@ cAudioManager::ProcessEngineDamage(cVehicleParams *params) m_sQueueSample.m_nFrequency = SampleManager.GetSampleBaseFrequency(SFX_CAR_ON_FIRE); } CalculateDistance(params->m_bDistanceCalculated, params->m_fDistance); - m_sQueueSample.m_bVolume = ComputeVolume(emittingVolume, 40.f, m_sQueueSample.m_fDistance); + m_sQueueSample.m_bVolume = ComputeVolume(emittingVolume, engineDamageIntensity, m_sQueueSample.m_fDistance); if(m_sQueueSample.m_bVolume) { m_sQueueSample.m_counter = 28; m_sQueueSample.m_bBankIndex = SAMPLEBANK_MAIN; @@ -3439,7 +3648,7 @@ cAudioManager::ProcessEngineDamage(cVehicleParams *params) SampleManager.GetSampleLoopStartOffset(m_sQueueSample.m_nSampleIndex); m_sQueueSample.m_nLoopEnd = SampleManager.GetSampleLoopEndOffset(m_sQueueSample.m_nSampleIndex); m_sQueueSample.field_48 = 2.0f; - m_sQueueSample.m_fSoundIntensity = 40.0f; + m_sQueueSample.m_fSoundIntensity = engineDamageIntensity; m_sQueueSample.field_56 = 0; m_sQueueSample.field_76 = 3; m_sQueueSample.m_bReverbFlag = true; @@ -3535,7 +3744,7 @@ cAudioManager::ProcessExplosions(int32 explosion) CVector *pos; float distSquared; - for(uint8 i = 0; i < 48; i++) { + for(uint8 i = 0; i < ARRAY_SIZE(gaExplosion); i++) { if(CExplosion::GetExplosionActiveCounter(i) == 1) { CExplosion::ResetExplosionActiveCounter(i); type = CExplosion::GetExplosionType(i); @@ -3906,7 +4115,7 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_bEmittingVolume = 60; \ m_sQueueSample.field_48 = 0.0f; \ m_sQueueSample.m_fSoundIntensity = 80.0f; \ - m_sQueueSample.field_16 = 4; \ + /*m_sQueueSample.field_16 = 4;*/ \ m_sQueueSample.m_bReverbFlag = true; \ /*m_sQueueSample.m_bReverbFlag = true;*/ \ m_sQueueSample.m_bIsDistant = false; \ @@ -3916,7 +4125,7 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_nLoopEnd = -1; \ m_sQueueSample.m_counter = iSound++; \ if(iSound < 32) iSound = 32; \ - m_sQueueSample.m_bRequireReflection = 1; \ + m_sQueueSample.m_bRequireReflection = true; \ AddSampleToRequestedQueue(); \ } \ } \ @@ -3936,7 +4145,7 @@ cAudioManager::ProcessGarages() state = CGarages::Garages[i].m_eGarageState; if(state == GS_OPENING || state == GS_CLOSING || state == GS_AFTERDROPOFF) { CalculateDistance(distCalculated, distSquared); - m_sQueueSample.m_bVolume = ComputeVolume(90u, 80.f, m_sQueueSample.m_fDistance); + m_sQueueSample.m_bVolume = ComputeVolume(90, 80.f, m_sQueueSample.m_fDistance); if(m_sQueueSample.m_bVolume) { if(CGarages::Garages[i].m_eGarageType == GARAGE_CRUSHER) { if(CGarages::Garages[i].m_eGarageState == GS_AFTERDROPOFF) { @@ -3956,11 +4165,11 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_nSampleIndex) >> 1; m_sQueueSample.m_nFrequency += - RandomDisplacement((int32)m_sQueueSample.m_nFrequency >> 4); + RandomDisplacement(m_sQueueSample.m_nFrequency >> 4); m_sQueueSample.m_nLoopCount = 1; m_sQueueSample.field_56 = 1; m_sQueueSample.m_counter = iSound++; - if(iSound < 32u) iSound = 32; + if(iSound < 32) iSound = 32; m_sQueueSample.m_bBankIndex = SAMPLEBANK_MAIN; m_sQueueSample.m_bIsDistant = false; m_sQueueSample.field_16 = 3; @@ -4002,9 +4211,9 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_bReverbFlag = true; m_sQueueSample.m_bRequireReflection = false; AddSampleToRequestedQueue(); - LOOP_HELPER } } + LOOP_HELPER } } #undef LOOP_HELPER @@ -4184,11 +4393,8 @@ cAudioManager::ProcessJumboAccel(CPlane *plane) void cAudioManager::ProcessJumboDecel(CPlane *plane) { - float modificator; - if(SetupJumboFlySound(20) && SetupJumboTaxiSound(75)) { - modificator = (plane->m_fSpeed - 0.10334f) * 1.676f; - if(modificator > 1.0f) modificator = 1.0f; + const float modificator = min(1.f, (plane->m_fSpeed - 0.10334f) * 1.676f); SetupJumboEngineSound(maxVolume * modificator, 6050.f * modificator + 16000); SetupJumboWhineSound(18, 29500); } @@ -4203,7 +4409,7 @@ cAudioManager::ProcessJumboFlying() void cAudioManager::ProcessJumboLanding(CPlane *plane) { - float modificator = (LandingPoint - PlanePathPosition[plane->m_nPlaneId]) / 350.f; + const float modificator = (LandingPoint - PlanePathPosition[plane->m_nPlaneId]) / 350.f; if(SetupJumboFlySound(107.f * modificator + 20)) { if(SetupJumboTaxiSound(75.f * (1.f - modificator))) { SetupJumboEngineSound(maxVolume, 22050); @@ -4215,7 +4421,7 @@ cAudioManager::ProcessJumboLanding(CPlane *plane) void cAudioManager::ProcessJumboTakeOff(CPlane *plane) { - float modificator = (PlanePathPosition[plane->m_nPlaneId] - TakeOffPoint) / 300.f; + const float modificator = (PlanePathPosition[plane->m_nPlaneId] - TakeOffPoint) / 300.f; if(SetupJumboFlySound((107.f * modificator) + 20) && SetupJumboRumbleSound(maxVolume * (1.f - modificator))) { if(SetupJumboEngineSound(maxVolume, 22050)) SetupJumboWhineSound(18.f * (1.f - modificator), 44100); @@ -5004,13 +5210,11 @@ cAudioManager::ProcessMissionAudio() void cAudioManager::ProcessModelCarEngine(cVehicleParams *params) { - cAudioManager *v2; CAutomobile *automobile; float allowedVelocity; int32 emittingVol; float velocityChange; - v2 = this; if(params->m_fDistance < 900.f) { automobile = (CAutomobile *)params->m_pVehicle; if(automobile->bEngineOn) { @@ -5312,13 +5516,13 @@ cAudioManager::ProcessPed(CPhysical *ped) { cPedParams params; - params.m_pPed = 0; - params.m_bDistanceCalculated = 0; + params.m_pPed = nil; + params.m_bDistanceCalculated = false; params.m_fDistance = 0.0f; m_sQueueSample.m_vecPos = ped->GetPosition(); - params.m_bDistanceCalculated = 0; + //params.m_bDistanceCalculated = false; params.m_pPed = (CPed *)ped; params.m_fDistance = GetDistanceSquared(&m_sQueueSample.m_vecPos); if(ped->m_modelIndex == MI_FATMALE02) ProcessPedHeadphones(¶ms); @@ -5329,7 +5533,7 @@ void cAudioManager::ProcessPedHeadphones(cPedParams *params) { CPed *ped; - CVehicle *veh; + CAutomobile *veh; uint8 emittingVol; if(params->m_fDistance < 49.f) { @@ -5338,9 +5542,9 @@ cAudioManager::ProcessPedHeadphones(cPedParams *params) CalculateDistance(params->m_bDistanceCalculated, params->m_fDistance); if(ped->bInVehicle && ped->m_nPedState == PED_DRIVING) { emittingVol = 10; - veh = ped->m_pMyVehicle; + veh = (CAutomobile*)ped->m_pMyVehicle; if(veh && veh->IsCar()) { - for(int32 i = 2; i < 6; i++) { + for(int32 i = 2; i < ARRAYSIZE(veh->Doors); i++) { if(!veh->IsDoorClosed((eDoors)i) || veh->IsDoorMissing((eDoors)i)) { emittingVol = 42; break; @@ -7235,11 +7439,224 @@ cAudioManager::ProcessVehicleDoors(cVehicleParams *params) return true; } -WRAPPER -bool -cAudioManager::ProcessVehicleEngine(cVehicleParams *params) +void +cAudioManager::ProcessVehicleEngine(cVehicleParams* params) { - EAXJMP(0x56A610); + CVehicle* playerVeh; + CVehicle* veh; + CAutomobile* automobile; + float relativeGearChange; + float relativeChange; + float reverseRelativechange; + uint8 volume; + eSfxSample accelerationSample; + int32 freq; + uint8 emittingVol; + cTransmission* transmission; + uint8 currentGear; + float modificator; + float traction = 0.f; + + if (params->m_fDistance < SQR(50.f)) { + playerVeh = FindPlayerVehicle(); + veh = params->m_pVehicle; + if (playerVeh == veh && veh->m_status == STATUS_WRECKED) { + SampleManager.StopChannel(m_bActiveSamples); + return; + } + if (veh->bEngineOn) { + CalculateDistance(params->m_bDistanceCalculated, params->m_fDistance); + automobile = (CAutomobile*)params->m_pVehicle; + if (params->m_nIndex == DODO) { + ProcessCesna(params); + return; + } + if (FindPlayerVehicle() == veh) { + ProcessPlayersVehicleEngine(params, automobile); + return; + } + transmission = params->m_pTransmission; + if (transmission) { + currentGear = params->m_pVehicle->m_nCurrentGear; + if (automobile->m_nWheelsOnGround) { + if (automobile->bIsHandbrakeOn) { + if (0.f == params->m_fVelocityChange) traction = 0.9f; + } + else if (params->m_pVehicle->m_status == STATUS_SIMPLE) { + traction = 0.f; + } + else { + switch (transmission->nDriveType) { + case '4': + for (int32 i = 0; i < ARRAY_SIZE(automobile->m_aWheelState); i++) { + if (automobile->m_aWheelState[i] == WHEEL_STATE_SPINNING) + traction += 0.05f; + } + break; + case 'F': + if (automobile->m_aWheelState[0] == WHEEL_STATE_SPINNING) + traction += 0.1f; + if (automobile->m_aWheelState[2] == WHEEL_STATE_SPINNING) + traction += 0.1f; + break; + case 'R': + if (automobile->m_aWheelState[1] == WHEEL_STATE_SPINNING) + traction += 0.1f; + if (automobile->m_aWheelState[3] == WHEEL_STATE_SPINNING) + traction += 0.1f; + break; + } + } + if (transmission->fMaxVelocity <= 0.f) { + relativeChange = 0.f; + } + else if (currentGear) { + if ((params->m_fVelocityChange - + transmission->Gears[currentGear].fShiftDownVelocity) / + transmission->fMaxVelocity * 2.5f <= + 1.f) + relativeGearChange = + (params->m_fVelocityChange - + transmission->Gears[currentGear].fShiftDownVelocity) / + transmission->fMaxVelocity * 2.5f; + else + relativeGearChange = 1.f; + if (0.f == traction && automobile->m_status != STATUS_SIMPLE && + params->m_fVelocityChange >= + transmission->Gears[1].fShiftUpVelocity) { + traction = 0.7f; + } + relativeChange = traction * automobile->m_fGasPedalAudio * 0.95f + + (1.f - traction) * relativeGearChange; + } + else { + reverseRelativechange = + Abs((params->m_fVelocityChange - + transmission->Gears[0].fShiftDownVelocity) / + transmission->fMaxReverseVelocity); + if (1.f - reverseRelativechange <= 1.f) { + relativeChange = 1.f - reverseRelativechange; + } + else { + relativeChange = 1.f; + } + } + } + else { + if (automobile->m_nDriveWheelsOnGround) + automobile->m_fGasPedalAudio = automobile->m_fGasPedalAudio * 0.4f; + relativeChange = automobile->m_fGasPedalAudio; + } + modificator = relativeChange; + if (currentGear || !automobile->m_nWheelsOnGround) + freq = 1200 * currentGear + 18000.f * modificator + 14000; + else + freq = 13000.f * modificator + 14000; + if (modificator >= 0.75f) { + emittingVol = 120; + volume = ComputeVolume(120, 50.f, m_sQueueSample.m_fDistance); + } + else { + emittingVol = modificator * 4 / 3 * 40.f + 80.f; + volume = ComputeVolume(emittingVol, 50.f, m_sQueueSample.m_fDistance); + } + } + else { + modificator = 0.f; + emittingVol = 80; + volume = ComputeVolume(80, 50.f, m_sQueueSample.m_fDistance); + } + m_sQueueSample.m_bVolume = volume; + if (m_sQueueSample.m_bVolume) { + if (automobile->m_status == STATUS_SIMPLE) { + if (modificator < 0.02f) { + m_sQueueSample.m_nSampleIndex = + CarSounds[params->m_nIndex].m_bEngineSoundType + SFX_CAR_REV_10; + freq = 10000.f * modificator + 22050; + m_sQueueSample.m_counter = 52; + m_sQueueSample.m_bBankIndex = 0; + m_sQueueSample.m_bIsDistant = 0; + m_sQueueSample.field_16 = 3; + m_sQueueSample.m_nFrequency = + freq + 100 * m_sQueueSample.m_nEntityIndex % 1000; + if (m_sQueueSample.m_nSampleIndex == SFX_CAR_IDLE_6 || + m_sQueueSample.m_nSampleIndex == SFX_CAR_REV_6) + m_sQueueSample.m_nFrequency = m_sQueueSample.m_nFrequency >> 1; + m_sQueueSample.m_nLoopCount = 0; + m_sQueueSample.m_bEmittingVolume = emittingVol; + m_sQueueSample.m_nLoopStart = SampleManager.GetSampleLoopStartOffset( + m_sQueueSample.m_nSampleIndex); + m_sQueueSample.m_nLoopEnd = + SampleManager.GetSampleLoopEndOffset(m_sQueueSample.m_nSampleIndex); + m_sQueueSample.field_48 = 6.0f; + m_sQueueSample.m_fSoundIntensity = 50.0f; + m_sQueueSample.field_56 = 0; + m_sQueueSample.field_76 = 8; + m_sQueueSample.m_bReverbFlag = 1; + m_sQueueSample.m_bRequireReflection = 0; + AddSampleToRequestedQueue(); + return; + } + accelerationSample = CarSounds[params->m_nIndex].m_nAccelerationSampleIndex; + } + else { + if (automobile->m_fGasPedal < 0.05f) { + m_sQueueSample.m_nSampleIndex = + CarSounds[params->m_nIndex].m_bEngineSoundType + + SFX_CAR_REV_10; // to recheck idle sounds start 1 postion later + freq = 10000.f * modificator + 22050; + m_sQueueSample.m_counter = 52; + m_sQueueSample.m_bBankIndex = 0; + m_sQueueSample.m_bIsDistant = 0; + m_sQueueSample.field_16 = 3; + m_sQueueSample.m_nFrequency = + freq + 100 * m_sQueueSample.m_nEntityIndex % 1000; + if (m_sQueueSample.m_nSampleIndex == SFX_CAR_IDLE_6 || + m_sQueueSample.m_nSampleIndex == SFX_CAR_REV_6) + m_sQueueSample.m_nFrequency = m_sQueueSample.m_nFrequency >> 1; + m_sQueueSample.m_nLoopCount = 0; + m_sQueueSample.m_bEmittingVolume = emittingVol; + m_sQueueSample.m_nLoopStart = SampleManager.GetSampleLoopStartOffset( + m_sQueueSample.m_nSampleIndex); + m_sQueueSample.m_nLoopEnd = + SampleManager.GetSampleLoopEndOffset(m_sQueueSample.m_nSampleIndex); + m_sQueueSample.field_48 = 6.0f; + m_sQueueSample.m_fSoundIntensity = 50.0f; + m_sQueueSample.field_56 = 0; + m_sQueueSample.field_76 = 8; + m_sQueueSample.m_bReverbFlag = 1; + m_sQueueSample.m_bRequireReflection = 0; + AddSampleToRequestedQueue(); + return; + } + accelerationSample = CarSounds[params->m_nIndex].m_nAccelerationSampleIndex; + } + m_sQueueSample.m_nSampleIndex = accelerationSample; + m_sQueueSample.m_counter = 2; + m_sQueueSample.m_bBankIndex = 0; + m_sQueueSample.m_bIsDistant = 0; + m_sQueueSample.field_16 = 3; + m_sQueueSample.m_nFrequency = freq + 100 * m_sQueueSample.m_nEntityIndex % 1000; + if (m_sQueueSample.m_nSampleIndex == SFX_CAR_IDLE_6 || + m_sQueueSample.m_nSampleIndex == SFX_CAR_REV_6) + m_sQueueSample.m_nFrequency = m_sQueueSample.m_nFrequency >> 1; + m_sQueueSample.m_nLoopCount = 0; + m_sQueueSample.m_bEmittingVolume = emittingVol; + m_sQueueSample.m_nLoopStart = + SampleManager.GetSampleLoopStartOffset(m_sQueueSample.m_nSampleIndex); + m_sQueueSample.m_nLoopEnd = + SampleManager.GetSampleLoopEndOffset(m_sQueueSample.m_nSampleIndex); + m_sQueueSample.field_48 = 6.0f; + m_sQueueSample.m_fSoundIntensity = 50.0f; + m_sQueueSample.field_56 = 0; + m_sQueueSample.field_76 = 8; + m_sQueueSample.m_bReverbFlag = 1; + m_sQueueSample.m_bRequireReflection = 0; + AddSampleToRequestedQueue(); + return; + } + } + } } void @@ -7359,7 +7776,7 @@ cAudioManager::ProcessVehicleRoadNoise(cVehicleParams *params) freq = 6050 * emittingVol / 30 + 16000; } else { m_sQueueSample.m_nSampleIndex = SFX_ROAD_NOISE; - modificator = m_sQueueSample.m_fDistance * 1.f / 95.f * 0.5f; + modificator = m_sQueueSample.m_fDistance / 190.f; sampleFreq = SampleManager.GetSampleBaseFrequency( SFX_ROAD_NOISE); freq = (sampleFreq * modificator) + ((3 * sampleFreq) >> 2); @@ -7648,7 +8065,7 @@ cAudioManager::ProcessWetRoadNoise(cVehicleParams *params) m_sQueueSample.m_bBankIndex = SAMPLEBANK_MAIN; m_sQueueSample.m_bIsDistant = false; m_sQueueSample.field_16 = 3; - modificator = m_sQueueSample.m_fDistance * 1.f / 3.f * 0.5f; + modificator = m_sQueueSample.m_fDistance / 6.f; freq = SampleManager.GetSampleBaseFrequency(SFX_ROAD_NOISE); m_sQueueSample.m_nFrequency = freq + freq * modificator; m_sQueueSample.m_nLoopCount = 0; diff --git a/src/audio/AudioManager.h b/src/audio/AudioManager.h index 70281237..0be1e38a 100644 --- a/src/audio/AudioManager.h +++ b/src/audio/AudioManager.h @@ -489,7 +489,7 @@ public: void PreloadMissionAudio(const char *name); /// ok void PreTerminateGameSpecificShutdown(); /// ok /// processX - main logic of adding new sounds - void ProcessActiveQueues(); // todo + void ProcessActiveQueues(); /// ok bool ProcessAirBrakes(cVehicleParams *params); /// ok void ProcessAirportScriptObject(uint8 sound); /// ok bool ProcessBoatEngine(cVehicleParams *params); /// ok @@ -544,7 +544,7 @@ public: bool ProcessTrainNoise(cVehicleParams *params); /// ok void ProcessVehicle(CVehicle *vehicle); /// ok bool ProcessVehicleDoors(cVehicleParams *params); /// ok - bool ProcessVehicleEngine(cVehicleParams *params); // todo + void ProcessVehicleEngine(cVehicleParams *params); /// ok void ProcessVehicleHorn(cVehicleParams *params); /// ok void ProcessVehicleOneShots(void *); // todo bool ProcessVehicleReverseWarning(cVehicleParams *params); /// ok From d13afe5cc132ee6755327edfed6f85e9936c096e Mon Sep 17 00:00:00 2001 From: Filip Gawin Date: Sat, 28 Mar 2020 19:20:08 +0100 Subject: [PATCH 38/70] audio16 fixes for review --- src/audio/AudioManager.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/audio/AudioManager.cpp b/src/audio/AudioManager.cpp index 2be8e36a..5410ed6f 100644 --- a/src/audio/AudioManager.cpp +++ b/src/audio/AudioManager.cpp @@ -2777,20 +2777,16 @@ cAudioManager::ProcessActiveQueues() sample.m_nSampleIndex == m_asActiveSamples[j].m_nSampleIndex) { if (sample.m_nLoopCount) { if (m_FrameCounter & 1) { - if (!(j & 1)) { flag = 0; } - flag = 1; + flag = !!(j & 1); } else { - if (!(j & 1)) - flag = 1; - else - flag = 0; + flag = !(j & 1); } if (flag && !SampleManager.GetChannelUsedFlag(j)) { sample.m_bLoopEnded = 1; m_asActiveSamples[j].m_bLoopEnded = 1; m_asActiveSamples[j].m_nSampleIndex = NO_SAMPLE; - m_asActiveSamples[j].m_nEntityIndex = -5; + m_asActiveSamples[j].m_nEntityIndex = AEHANDLE_NONE; continue; } } @@ -4115,7 +4111,7 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_bEmittingVolume = 60; \ m_sQueueSample.field_48 = 0.0f; \ m_sQueueSample.m_fSoundIntensity = 80.0f; \ - /*m_sQueueSample.field_16 = 4;*/ \ + /*m_sQueueSample.field_16 = 4;*/ \ m_sQueueSample.m_bReverbFlag = true; \ /*m_sQueueSample.m_bReverbFlag = true;*/ \ m_sQueueSample.m_bIsDistant = false; \ @@ -4125,7 +4121,7 @@ cAudioManager::ProcessGarages() m_sQueueSample.m_nLoopEnd = -1; \ m_sQueueSample.m_counter = iSound++; \ if(iSound < 32) iSound = 32; \ - m_sQueueSample.m_bRequireReflection = true; \ + m_sQueueSample.m_bRequireReflection = true; \ AddSampleToRequestedQueue(); \ } \ } \ From 39c9a0582700ab7242272952edbe211a4dd13935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Sat, 28 Mar 2020 23:28:36 +0300 Subject: [PATCH 39/70] Limit frontend FPS to 100 --- src/skel/win/win.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/skel/win/win.cpp b/src/skel/win/win.cpp index f05580cd..4e5dccff 100644 --- a/src/skel/win/win.cpp +++ b/src/skel/win/win.cpp @@ -2056,7 +2056,13 @@ _WinMain(HINSTANCE instance, { GetWindowPlacement(PSGLOBAL(window), &wp); - if ( wp.showCmd != SW_SHOWMINIMIZED ) + // Famous transparent menu bug. Also see the fix in Frontend.cpp +#ifdef FIX_BUGS + float ms = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); + if ((1000.0f / 100.0f) < ms && wp.showCmd != SW_SHOWMINIMIZED) +#else + if (wp.showCmd != SW_SHOWMINIMIZED) +#endif RsEventHandler(rsFRONTENDIDLE, nil); if ( !FrontEndMenuManager.m_bMenuActive || FrontEndMenuManager.m_bLoadingSavedGame ) From 93417853ed36879d6b504857cae662b5c9c519bd Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sat, 28 Mar 2020 23:41:37 +0300 Subject: [PATCH 40/70] fixes --- src/control/Garages.cpp | 205 +++++++++++++++++++++++++--------------- src/control/Garages.h | 6 +- 2 files changed, 134 insertions(+), 77 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 27392591..672c3381 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -3,12 +3,14 @@ #include "Garages.h" #include "main.h" +#include "DMAudio.h" #include "General.h" #include "Font.h" #include "HandlingMgr.h" #include "Hud.h" #include "Messages.h" #include "ModelIndices.h" +#include "Pad.h" #include "Particle.h" #include "PlayerPed.h" #include "Replay.h" @@ -16,10 +18,11 @@ #include "Text.h" #include "Timer.h" #include "Vehicle.h" +#include "Wanted.h" #include "World.h" #define CRUSHER_GARAGE_X1 (1135.5f) -#define CRUSHER_GARAGE_Y1 (7.0f) +#define CRUSHER_GARAGE_Y1 (57.0f) #define CRUSHER_GARAGE_Z1 (-1.0f) #define CRUSHER_GARAGE_X2 (1149.5f) #define CRUSHER_GARAGE_Y2 (63.7f) @@ -29,61 +32,61 @@ #define ROTATED_DOOR_CLOSE_SPEED (0.02f) #define DEFAULT_DOOR_OPEN_SPEED (0.035f) #define DEFAULT_DOOR_CLOSE_SPEED (0.04f) -#define CRUSHER_CRANE_SPEED 0.005f +#define CRUSHER_CRANE_SPEED (0.005f) // Prices -#define BOMB_PRICE 1000 -#define RESPRAY_PRICE 1000 +#define BOMB_PRICE (1000) +#define RESPRAY_PRICE (1000) // Distances -#define DISTANCE_TO_CALL_OFF_CHASE 10.0f -#define DISTANCE_FOR_MRWHOOP_HACK 4.0f -#define DISTANCE_TO_ACTIVATE_GARAGE 8.0f -#define DISTANCE_TO_ACTIVATE_KEEPCAR_GARAGE 17.0f -#define DISTANCE_TO_CLOSE_MISSION_GARAGE 30.0f -#define DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE 25.0 -#define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE 40.0f -#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT 2.4f -#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR 15.0f -#define DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE 70.0f -#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT 1.7f -#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_IN_CAR 10.0f -#define DISTANCE_TO_SHOW_HIDEOUT_MESSAGE 5.0f +#define DISTANCE_TO_CALL_OFF_CHASE (10.0f) +#define DISTANCE_FOR_MRWHOOP_HACK (4.0f) +#define DISTANCE_TO_ACTIVATE_GARAGE (8.0f) +#define DISTANCE_TO_ACTIVATE_KEEPCAR_GARAGE (17.0f) +#define DISTANCE_TO_CLOSE_MISSION_GARAGE (30.0f) +#define DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE (25.0f) +#define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE (40.0f) +#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT (2.4f) +#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR (15.0f) +#define DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE (70.0f) +#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT (1.7f) +#define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_IN_CAR (10.0f) +#define DISTANCE_TO_SHOW_HIDEOUT_MESSAGE (5.0f) // Time -#define TIME_TO_RESPRAY 2000 -#define TIME_TO_SETUP_BOMB 2000 -#define TIME_TO_CRUSH_CAR 3000 -#define TIME_TO_PROCESS_KEEPCAR_GARAGE 2000 +#define TIME_TO_RESPRAY (2000) +#define TIME_TO_SETUP_BOMB (2000) +#define TIME_TO_CRUSH_CAR (3000) +#define TIME_TO_PROCESS_KEEPCAR_GARAGE (2000) // Respray stuff -#define FREE_RESPRAY_HEALTH_THRESHOLD 970.0f -#define NUM_PARTICLES_IN_RESPRAY 200 +#define FREE_RESPRAY_HEALTH_THRESHOLD (970.0f) +#define NUM_PARTICLES_IN_RESPRAY (200) // Bomb stuff -#define KGS_OF_EXPLOSIVES_IN_BOMB 10 +#define KGS_OF_EXPLOSIVES_IN_BOMB (10) // Collect specific cars stuff -#define REWARD_FOR_FIRST_POLICE_CAR 5000 -#define REWARD_FOR_FIRST_BANK_VAN 5000 -#define MAX_POLICE_CARS_TO_COLLECT 10 -#define MAX_BANK_VANS_TO_COLLECT 10 +#define REWARD_FOR_FIRST_POLICE_CAR (5000) +#define REWARD_FOR_FIRST_BANK_VAN (5000) +#define MAX_POLICE_CARS_TO_COLLECT (10) +#define MAX_BANK_VANS_TO_COLLECT (10) // Collect cars stuff -#define MAX_SPEED_TO_SHOW_COLLECTED_MESSAGE 0.03f +#define MAX_SPEED_TO_SHOW_COLLECTED_MESSAGE (0.03f) // Crusher stuff -#define CRUSHER_VEHICLE_TEST_SPAN 8 -#define CRUSHER_MIN_REWARD 25 -#define CRUSHER_MAX_REWARD 125 -#define CRUSHER_REWARD_COEFFICIENT 1.0f/500000 +#define CRUSHER_VEHICLE_TEST_SPAN (8) +#define CRUSHER_MIN_REWARD (25) +#define CRUSHER_MAX_REWARD (125) +#define CRUSHER_REWARD_COEFFICIENT (1.0f/500000) // Hideout stuff -#define MAX_STORED_CARS_IN_INDUSTRIAL 1 -#define MAX_STORED_CARS_IN_COMMERCIAL NUM_GARAGE_STORED_CARS -#define MAX_STORED_CARS_IN_SUBURBAN NUM_GARAGE_STORED_CARS -#define HIDEOUT_DOOR_SPEED_COEFFICIENT 1.7f -#define TIME_BETWEEN_HIDEOUT_MESSAGES 18000 +#define MAX_STORED_CARS_IN_INDUSTRIAL (1) +#define MAX_STORED_CARS_IN_COMMERCIAL (NUM_GARAGE_STORED_CARS) +#define MAX_STORED_CARS_IN_SUBURBAN (NUM_GARAGE_STORED_CARS) +#define HIDEOUT_DOOR_SPEED_COEFFICIENT (1.7f) +#define TIME_BETWEEN_HIDEOUT_MESSAGES (18000) int32 &CGarages::BankVansCollected = *(int32 *)0x8F1B34; bool &CGarages::BombsAreFree = *(bool *)0x95CD7A; @@ -192,7 +195,7 @@ int16 CGarages::AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z pGarage->m_fDoor1Z = Z1; pGarage->m_fDoor2Z = Z1; pGarage->m_eGarageType = type; - pGarage->field_24 = 0; + pGarage->m_bRecreateDoorOnNextRefresh = false; pGarage->m_bRotatedDoor = false; pGarage->m_bCameraFollowsPlayer = false; pGarage->RefreshDoorPointers(true); @@ -281,16 +284,18 @@ void CGarage::Update() TheCamera.pToGarageWeAreIn = this; CGarages::bCamShouldBeOutisde = true; } - if (pVehicle && IsEntityEntirelyOutside(pVehicle, 0.0f)) - TheCamera.pToGarageWeAreInForHackAvoidFirstPerson = this; - if (pVehicle->GetModelIndex() == MI_MRWHOOP) { - if (pVehicle->IsWithinArea( - m_fX1 - DISTANCE_FOR_MRWHOOP_HACK, - m_fX2 + DISTANCE_FOR_MRWHOOP_HACK, - m_fY1 - DISTANCE_FOR_MRWHOOP_HACK, - m_fY2 + DISTANCE_FOR_MRWHOOP_HACK)) { - TheCamera.pToGarageWeAreIn = this; - CGarages::bCamShouldBeOutisde = true; + if (pVehicle) { + if (IsEntityEntirelyOutside(pVehicle, 0.0f)) + TheCamera.pToGarageWeAreInForHackAvoidFirstPerson = this; + if (pVehicle->GetModelIndex() == MI_MRWHOOP) { + if (pVehicle->IsWithinArea( + m_fX1 - DISTANCE_FOR_MRWHOOP_HACK, + m_fX2 + DISTANCE_FOR_MRWHOOP_HACK, + m_fY1 - DISTANCE_FOR_MRWHOOP_HACK, + m_fY2 + DISTANCE_FOR_MRWHOOP_HACK)) { + TheCamera.pToGarageWeAreIn = this; + CGarages::bCamShouldBeOutisde = true; + } } } } @@ -329,7 +334,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_RESPRAY; @@ -426,7 +431,7 @@ void CGarage::Update() m_fY2 + DISTANCE_TO_CALL_OFF_CHASE); break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -471,7 +476,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_SETUP_BOMB; @@ -530,7 +535,7 @@ void CGarage::Update() break; } case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -564,7 +569,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); if (m_bClosingWithoutTargetCar) @@ -593,7 +598,7 @@ void CGarage::Update() } break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -630,7 +635,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -677,7 +682,7 @@ void CGarage::Update() m_pTarget = FindPlayerVehicle(); m_pTarget->RegisterReference((CEntity**)&m_pTarget); } - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -726,7 +731,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -766,7 +771,7 @@ void CGarage::Update() m_pTarget = FindPlayerVehicle(); m_pTarget->RegisterReference((CEntity**)&m_pTarget); } - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -787,7 +792,7 @@ void CGarage::Update() m_eGarageState = GS_CLOSING; break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -798,7 +803,7 @@ void CGarage::Update() case GS_FULLYCLOSED: break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -901,7 +906,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); if (m_bClosingWithoutTargetCar) @@ -929,7 +934,7 @@ void CGarage::Update() m_eGarageState = GS_OPENING; break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -949,7 +954,7 @@ void CGarage::Update() case GARAGE_FOR_SCRIPT_TO_OPEN: switch (m_eGarageState) { case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -969,7 +974,7 @@ void CGarage::Update() case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: switch (m_eGarageState) { case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -977,7 +982,7 @@ void CGarage::Update() UpdateDoorsHeight(); break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1022,9 +1027,9 @@ void CGarage::Update() #ifndef FIX_BUGS // TODO: check and replace with ifdef if (!IsPlayerOutsideGarage()) m_eGarageState = GS_OPENING; - m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); #else - m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (!IsPlayerOutsideGarage()) m_eGarageState = GS_OPENING; #endif @@ -1043,7 +1048,7 @@ void CGarage::Update() { float distance = CalcDistToGarageRectangleSquared(FindPlayerCoors().x, FindPlayerCoors().y); if (distance < SQR(DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT) || - distance < SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR) && FindPlayerVehicle()) { + distance < SQR(DISTANCE_TO_OPEN_HIDEOUT_GARAGE_IN_CAR) && FindPlayerVehicle()) { if (FindPlayerVehicle() && CGarages::CountCarsInHideoutGarage(m_eGarageType) >= CGarages::FindMaxNumStoredCarsForGarage(m_eGarageType)) { if (m_pDoor1) { if (((CVector2D)FindPlayerVehicle()->GetPosition() - (CVector2D)m_pDoor1->GetPosition()).MagnitudeSqr() < SQR(DISTANCE_TO_SHOW_HIDEOUT_MESSAGE) && @@ -1071,7 +1076,7 @@ void CGarage::Update() break; } case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + HIDEOUT_DOOR_SPEED_COEFFICIENT * m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1096,7 +1101,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -1112,7 +1117,7 @@ void CGarage::Update() m_eGarageState = GS_OPENING; break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1192,9 +1197,59 @@ void CGarages::PrintMessages() WRAPPER bool CGarages::IsCarSprayable(CVehicle*) { EAXJMP(0x426700); } WRAPPER void CGarage::UpdateDoorsHeight() { EAXJMP(0x426730); } WRAPPER void CGarage::BuildRotatedDoorMatrix(CEntity*, float) { EAXJMP(0x4267C0); } -WRAPPER void CGarage::UpdateCrusherAngle() { EAXJMP(0x4268A0); } + +void CGarage::UpdateCrusherAngle() +{ + RefreshDoorPointers(false); + m_pDoor2->GetMatrix().SetRotateXOnly(TWOPI - m_fDoorPos); + m_pDoor2->GetMatrix().UpdateRW(); + m_pDoor2->UpdateRwFrame(); +} + WRAPPER void CGarage::UpdateCrusherShake(float, float) { EAXJMP(0x4268E0); } -WRAPPER void CGarage::RefreshDoorPointers(bool) { EAXJMP(0x426980); } + +// This is dumb but there is no way to avoid goto. What was there originally even? +static bool DoINeedToRefreshPointer(CEntity* pDoor, bool bIsDummy, int8 nIndex) +{ + bool bNeedToFindDoorEntities = false; + if (pDoor) { + if (bIsDummy) { + if (CPools::GetDummyPool()->IsFreeSlot(CPools::GetDummyPool()->GetJustIndex((CDummy*)pDoor))) + return true; + if (nIndex != CPools::GetDummyPool()->GetIndex((CDummy*)pDoor)) + bNeedToFindDoorEntities = true; + if (!CGarages::IsModelIndexADoor(pDoor->GetModelIndex())) + return true; + } + else { + if (CPools::GetObjectPool()->IsFreeSlot(CPools::GetObjectPool()->GetJustIndex((CObject*)pDoor))) + return true; + if (nIndex != CPools::GetObjectPool()->GetIndex((CObject*)pDoor)) + bNeedToFindDoorEntities = true; + if (!CGarages::IsModelIndexADoor(pDoor->GetModelIndex())) + return true; + } + } + return bNeedToFindDoorEntities; +} + +void CGarage::RefreshDoorPointers(bool bCreate) +{ + bool bNeedToFindDoorEntities = true; + if (!bCreate && !m_bRecreateDoorOnNextRefresh) + bNeedToFindDoorEntities = false; + if (DoINeedToRefreshPointer(m_pDoor1, m_bDoor1IsDummy, m_bDoor1PoolIndex)) + bNeedToFindDoorEntities = true; + if (DoINeedToRefreshPointer(m_pDoor2, m_bDoor2IsDummy, m_bDoor2PoolIndex)) + bNeedToFindDoorEntities = true; + if (bNeedToFindDoorEntities) + FindDoorsEntities(); + if (m_pDoor1 && bCreate) + debug("Created door 1 for type %d", m_eGarageType); + if (m_pDoor2 && bCreate) + debug("Created door 2 for type %d", m_eGarageType); +} + WRAPPER void CGarages::TriggerMessage(const char* text, int16, uint16 time, int16) { EAXJMP(0x426B20); } WRAPPER void CGarages::SetTargetCarForMissonGarage(int16, CVehicle*) { EAXJMP(0x426BD0); } WRAPPER bool CGarages::HasCarBeenDroppedOffYet(int16) { EAXJMP(0x426C20); } @@ -1356,4 +1411,6 @@ STARTPATCHES #ifndef PS2 InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); #endif + InjectHook(0x421E40, CGarages::Update, PATCH_JUMP); + InjectHook(0x4268A0, &CGarage::UpdateCrusherAngle, PATCH_JUMP); ENDPATCHES \ No newline at end of file diff --git a/src/control/Garages.h b/src/control/Garages.h index f12ccd0f..e39a81fa 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -89,9 +89,9 @@ public: CEntity *m_pDoor2; uint8 m_bDoor1PoolIndex; uint8 m_bDoor2PoolIndex; - bool m_bIsDoor1Object; - bool m_bIsDoor2Object; - char field_24; + bool m_bDoor1IsDummy; + bool m_bDoor2IsDummy; + bool m_bRecreateDoorOnNextRefresh; bool m_bRotatedDoor; bool m_bCameraFollowsPlayer; float m_fX1; From 194ddc5f40835476673655bd3895f2b7fe7fee0c Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Sat, 28 Mar 2020 23:55:23 +0300 Subject: [PATCH 41/70] CWeaponEffects(autoaim crosshair) done, CGame done. restored some original R* names --- src/control/GameLogic.cpp | 2 +- src/control/Gangs.cpp | 6 +- src/control/Gangs.h | 2 +- src/control/Garages.cpp | 4 + src/control/Garages.h | 3 + src/control/Script.cpp | 4 +- src/control/Script.h | 2 +- src/core/CutsceneMgr.cpp | 5 +- src/core/Game.cpp | 449 +++++++++++++++++++++++++++++++---- src/core/Game.h | 19 +- src/core/RwHelper.cpp | 20 +- src/core/RwHelper.h | 9 +- src/core/TxdStore.cpp | 4 +- src/core/TxdStore.h | 2 +- src/core/World.cpp | 1 + src/core/World.h | 1 + src/core/main.cpp | 3 +- src/core/main.h | 1 + src/peds/Ped.cpp | 26 +- src/render/Rubbish.cpp | 1 + src/render/Rubbish.h | 1 + src/render/Skidmarks.cpp | 1 + src/render/Skidmarks.h | 1 + src/render/SpecialFX.cpp | 1 + src/render/SpecialFX.h | 1 + src/render/WeaponEffects.cpp | 111 +++++++-- src/render/WeaponEffects.h | 30 ++- src/vehicles/Automobile.cpp | 4 +- src/weapons/Weapon.cpp | 1 + src/weapons/Weapon.h | 13 +- 30 files changed, 596 insertions(+), 132 deletions(-) diff --git a/src/control/GameLogic.cpp b/src/control/GameLogic.cpp index 1493cec0..0abae7d6 100644 --- a/src/control/GameLogic.cpp +++ b/src/control/GameLogic.cpp @@ -57,7 +57,7 @@ CGameLogic::SortOutStreamingAndMemory(const CVector &pos) CStreaming::FlushRequestList(); CStreaming::DeleteRwObjectsAfterDeath(pos); CStreaming::RemoveUnusedModelsInLoadedList(); - CGame::DrasticTidyUpMemory(); + CGame::DrasticTidyUpMemory(true); CStreaming::LoadScene(pos); CTimer::Update(); } diff --git a/src/control/Gangs.cpp b/src/control/Gangs.cpp index 340fe0f6..ac32ad98 100644 --- a/src/control/Gangs.cpp +++ b/src/control/Gangs.cpp @@ -14,7 +14,7 @@ CGangInfo::CGangInfo() : m_Weapon2(WEAPONTYPE_UNARMED) {} -void CGangs::Initialize(void) +void CGangs::Initialise(void) { Gang[GANG_MAFIA].m_nVehicleMI = MI_MAFIA; Gang[GANG_TRIAD].m_nVehicleMI = MI_BELLYUP; @@ -67,7 +67,7 @@ VALIDATESAVEBUF(*size); void CGangs::LoadAllGangData(uint8 *buf, uint32 size) { - Initialize(); + Initialise(); INITSAVEBUF // original: SkipSaveBuf(buf, SAVE_HEADER_SIZE); @@ -79,7 +79,7 @@ VALIDATESAVEBUF(size); } STARTPATCHES - InjectHook(0x4C3FB0, CGangs::Initialize, PATCH_JUMP); + InjectHook(0x4C3FB0, CGangs::Initialise, PATCH_JUMP); InjectHook(0x4C4010, CGangs::SetGangVehicleModel, PATCH_JUMP); InjectHook(0x4C4030, CGangs::SetGangWeapons, PATCH_JUMP); InjectHook(0x4C4050, CGangs::SetGangPedModelOverride, PATCH_JUMP); diff --git a/src/control/Gangs.h b/src/control/Gangs.h index cf22cc73..dd7a7f93 100644 --- a/src/control/Gangs.h +++ b/src/control/Gangs.h @@ -28,7 +28,7 @@ enum { class CGangs { public: - static void Initialize(void); + static void Initialise(void); static void SetGangVehicleModel(int16, int32); static void SetGangWeapons(int16, int32, int32); static void SetGangPedModelOverride(int16, int8); diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 5ac15377..7a28bdc8 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -30,10 +30,14 @@ uint32 &CGarages::GarageToBeTidied = *(uint32 *)0x623570; CGarage(&CGarages::Garages)[NUM_GARAGES] = *(CGarage(*)[NUM_GARAGES])*(uintptr*)0x72BCD0; WRAPPER void CGarages::Init(void) { EAXJMP(0x421C60); } +WRAPPER void CGarages::Shutdown(void) { EAXJMP(0x421E10); } + WRAPPER void CGarages::Update(void) { EAXJMP(0x421E40); } WRAPPER void CGarages::Load(uint8* buf, uint32 size) { EAXJMP(0x428940); } WRAPPER void CGarages::Save(uint8* buf, uint32 *size) { EAXJMP(0x4284e0); } +WRAPPER void CGarages::SetAllDoorsBackToOriginalHeight(void) { EAXJMP(0x4283D0); } + bool CGarages::IsModelIndexADoor(uint32 id) { diff --git a/src/control/Garages.h b/src/control/Garages.h index 5e106ade..783eb1e1 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -146,6 +146,7 @@ public: static bool IsPointWithinAnyGarage(CVector&); static void PlayerArrestedOrDied(); static void Init(void); + static void Shutdown(void); static void Update(void); static void Load(uint8 *buf, uint32 size); static void Save(uint8 *buf, uint32 *size); @@ -167,4 +168,6 @@ public: static bool IsThisCarWithinGarageArea(int16, CEntity*); static int GetCarsCollectedIndexForGarageType(eGarageType type) { return type - GARAGE_COLLECTCARS_1; } + + static void SetAllDoorsBackToOriginalHeight(); }; diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 14f55734..d989a0ce 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -9819,7 +9819,7 @@ void CTheScripts::UndoBuildingSwaps() } } -void CTheScripts::UndoEntityVisibilitySettings() +void CTheScripts::UndoEntityInvisibilitySettings() { for (int i = 0; i < MAX_NUM_INVISIBILITY_SETTINGS; i++) { if (InvisibilitySettingArray[i]) { @@ -11657,7 +11657,7 @@ InjectHook(0x439040, &CTheScripts::Process, PATCH_JUMP); InjectHook(0x439400, &CTheScripts::StartTestScript, PATCH_JUMP); InjectHook(0x439410, &CTheScripts::IsPlayerOnAMission, PATCH_JUMP); InjectHook(0x44FD10, &CTheScripts::UndoBuildingSwaps, PATCH_JUMP); -InjectHook(0x44FD60, &CTheScripts::UndoEntityVisibilitySettings, PATCH_JUMP); +InjectHook(0x44FD60, &CTheScripts::UndoEntityInvisibilitySettings, PATCH_JUMP); InjectHook(0x4534E0, &CTheScripts::ScriptDebugLine3D, PATCH_JUMP); InjectHook(0x453550, &CTheScripts::RenderTheScriptDebugLines, PATCH_JUMP); InjectHook(0x4535E0, &CTheScripts::SaveAllScripts, PATCH_JUMP); diff --git a/src/control/Script.h b/src/control/Script.h index b6844b6c..fbcdce48 100644 --- a/src/control/Script.h +++ b/src/control/Script.h @@ -281,7 +281,7 @@ public: static void ClearSpaceForMissionEntity(const CVector&, CEntity*); static void UndoBuildingSwaps(); - static void UndoEntityVisibilitySettings(); + static void UndoEntityInvisibilitySettings(); static void ScriptDebugLine3D(float x1, float y1, float z1, float x2, float y2, float z2, uint32 col, uint32 col2); static void RenderTheScriptDebugLines(); diff --git a/src/core/CutsceneMgr.cpp b/src/core/CutsceneMgr.cpp index a3ff2fd0..283f34b8 100644 --- a/src/core/CutsceneMgr.cpp +++ b/src/core/CutsceneMgr.cpp @@ -183,7 +183,7 @@ CCutsceneMgr::LoadCutsceneData(const char *szCutsceneName) ms_pCutsceneDir->ReadDirFile("ANIM\\CUTS.DIR"); CStreaming::RemoveUnusedModelsInLoadedList(); - CGame::DrasticTidyUpMemory(); + CGame::DrasticTidyUpMemory(true); strcpy(ms_cutsceneName, szCutsceneName); file = CFileMgr::OpenFile("ANIM\\CUTS.IMG", "rb"); @@ -374,8 +374,7 @@ CCutsceneMgr::DeleteCutsceneData(void) DMAudio.ChangeMusicMode(MUSICMODE_GAME); } CTimer::Stop(); - //TheCamera.GetScreenFadeStatus() == 2; // what for?? - CGame::DrasticTidyUpMemory(); + CGame::DrasticTidyUpMemory(TheCamera.GetScreenFadeStatus() == 2); CTimer::Update(); } diff --git a/src/core/Game.cpp b/src/core/Game.cpp index fce0c67f..57683893 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -1,7 +1,14 @@ +#pragma warning( push ) +#pragma warning( disable : 4005) +#define DIRECTINPUT_VERSION 0x0800 +#include +#pragma warning( pop ) #include "common.h" +#include "win.h" #include "patcher.h" #include "Game.h" #include "main.h" +#include "RwHelper.h" #include "Accident.h" #include "Antennas.h" #include "Bridge.h" @@ -17,6 +24,7 @@ #include "Cranes.h" #include "Credits.h" #include "CutsceneMgr.h" +#include "DMAudio.h" #include "Darkel.h" #include "Debug.h" #include "EventList.h" @@ -28,26 +36,32 @@ #include "Frontend.h" #include "GameLogic.h" #include "Garages.h" +#include "GenericGameStorage.h" #include "Glass.h" +#include "HandlingMgr.h" #include "Heli.h" +#include "Hud.h" #include "IniFile.h" +#include "Lights.h" +#include "MBlur.h" #include "Messages.h" #include "Pad.h" #include "Particle.h" +#include "ParticleObject.h" +#include "PedRoutes.h" #include "Phones.h" #include "Pickups.h" #include "Plane.h" +#include "PlayerSkin.h" #include "Population.h" +#include "Radar.h" #include "Record.h" +#include "References.h" #include "Renderer.h" #include "Replay.h" -#include "References.h" -#include "Radar.h" #include "Restart.h" #include "RoadBlocks.h" -#include "PedRoutes.h" #include "Rubbish.h" -#include "RwHelper.h" #include "SceneEdit.h" #include "Script.h" #include "Shadows.h" @@ -56,11 +70,14 @@ #include "Sprite2d.h" #include "Stats.h" #include "Streaming.h" +#include "SurfaceTable.h" +#include "TempColModels.h" #include "TimeCycle.h" #include "TrafficLights.h" #include "Train.h" #include "TxdStore.h" #include "User.h" +#include "VisibilityPlugins.h" #include "WaterCannon.h" #include "WaterLevel.h" #include "Weapon.h" @@ -70,6 +87,10 @@ #include "ZoneCull.h" #include "Zones.h" + + +#define DEFAULT_VIEWWINDOW (0.7f) + eLevelName &CGame::currLevel = *(eLevelName*)0x941514; bool &CGame::bDemoMode = *(bool*)0x5F4DD0; bool &CGame::nastyGame = *(bool*)0x5F4DD4; @@ -79,6 +100,7 @@ bool &CGame::noProstitutes = *(bool*)0x95CDCF; bool &CGame::playingIntro = *(bool*)0x95CDC2; char *CGame::aDatFile = (char*)0x773A48; +int &gameTxdSlot = *(int*)0x628D88; bool CGame::InitialiseOnceBeforeRW(void) @@ -89,7 +111,143 @@ CGame::InitialiseOnceBeforeRW(void) return true; } -int &gameTxdSlot = *(int*)0x628D88; +bool +CGame::InitialiseRenderWare(void) +{ + _TexturePoolsInitialise(); + + CTxdStore::Initialise(); + CVisibilityPlugins::Initialise(); + + /* Create camera */ + Scene.camera = CameraCreate(RsGlobal.width, RsGlobal.height, TRUE); + ASSERT(Scene.camera != NULL); + if (!Scene.camera) + { + return (false); + } + + RwCameraSetFarClipPlane(Scene.camera, (RwReal) (2000.0)); + RwCameraSetNearClipPlane(Scene.camera, (RwReal) (0.9)); + + CameraSize(Scene.camera, NULL, DEFAULT_VIEWWINDOW, DEFAULT_ASPECT_RATIO); + + /* Create a world */ + RwBBox bbox; + + bbox.sup.x = bbox.sup.y = bbox.sup.z = (RwReal)(10000.0); + bbox.inf.x = bbox.inf.y = bbox.inf.z = (RwReal)(-10000.0); + + Scene.world = RpWorldCreate(&bbox); + ASSERT(Scene.world != NULL); + if (!Scene.world) + { + CameraDestroy(Scene.camera); + Scene.camera = NULL; + return (false); + } + + /* Add the camera to the world */ + RpWorldAddCamera(Scene.world, Scene.camera); + LightsCreate(Scene.world); + + CreateDebugFont(); + + CFont::Initialise(); + CHud::Initialise(); + CPlayerSkin::Initialise(); + + return (true); +} + +void CGame::ShutdownRenderWare(void) +{ + CMBlur::MotionBlurClose(); + DestroySplashScreen(); + CHud::Shutdown(); + CFont::Shutdown(); + + for ( int32 i = 0; i < NUMPLAYERS; i++ ) + CWorld::Players[i].DeletePlayerSkin(); + + CPlayerSkin::Shutdown(); + + DestroyDebugFont(); + + /* Destroy world */ + LightsDestroy(Scene.world); + RpWorldRemoveCamera(Scene.world, Scene.camera); + RpWorldDestroy(Scene.world); + + /* destroy camera */ + CameraDestroy(Scene.camera); + + Scene.world = NULL; + Scene.camera = NULL; + + CVisibilityPlugins::Shutdown(); + + _TexturePoolsShutdown(); +} + +bool CGame::InitialiseOnceAfterRW(void) +{ + TheText.Load(); + DMAudio.Initialise(); + CTimer::Initialise(); + CTempColModels::Initialise(); + mod_HandlingManager.Initialise(); + CSurfaceTable::Initialise("DATA\\SURFACE.DAT"); + CPedStats::Initialise(); + CTimeCycle::Initialise(); + + if ( DMAudio.GetNum3DProvidersAvailable() == 0 ) + FrontEndMenuManager.m_nPrefsAudio3DProviderIndex = -1; + + if ( FrontEndMenuManager.m_nPrefsAudio3DProviderIndex == -99 || FrontEndMenuManager.m_nPrefsAudio3DProviderIndex == -2 ) + { + CMenuManager::m_PrefsSpeakers = 0; + + for ( int32 i = 0; i < DMAudio.GetNum3DProvidersAvailable(); i++ ) + { + wchar buff[64]; + + char *name = DMAudio.Get3DProviderName(i); + AsciiToUnicode(name, buff); + char *providername = UnicodeToAscii(buff); + strupr(providername); + + if ( !strcmp(providername, "MILES FAST 2D POSITIONAL AUDIO") ) + { + FrontEndMenuManager.m_nPrefsAudio3DProviderIndex = i; + break; + } + } + } + + DMAudio.SetCurrent3DProvider(FrontEndMenuManager.m_nPrefsAudio3DProviderIndex); + DMAudio.SetSpeakerConfig(CMenuManager::m_PrefsSpeakers); + DMAudio.SetDynamicAcousticModelingStatus(CMenuManager::m_PrefsDMA); + DMAudio.SetMusicMasterVolume(CMenuManager::m_PrefsMusicVolume); + DMAudio.SetEffectsMasterVolume(CMenuManager::m_PrefsSfxVolume); + DMAudio.SetEffectsFadeVol(127); + DMAudio.SetMusicFadeVol(127); + CWorld::Players[0].SetPlayerSkin(CMenuManager::m_PrefsSkinFile); + + return true; +} + +#if 0 +WRAPPER void CGame::FinalShutdown(void) { EAXJMP(0x48BEC0); } +#else +void +CGame::FinalShutdown(void) +{ + CTxdStore::Shutdown(); + CPedStats::Shutdown(); + CdStreamShutdown(); +} +#endif bool CGame::Initialise(const char* datFile) { @@ -160,7 +318,7 @@ bool CGame::Initialise(const char* datFile) CStreaming::Init(); } else { CStreaming::Init(); - if (ConvertTextures()) { + if (CreateTxdImageForVideoCard()) { CStreaming::Shutdown(); CdStreamAddImage("MODELS\\TXD.IMG"); CStreaming::Init(); @@ -198,7 +356,7 @@ bool CGame::Initialise(const char* datFile) CSceneEdit::Init(); LoadingScreen("Loading the Game", "Load scripts", nil); CTheScripts::Init(); - CGangs::Initialize(); + CGangs::Initialise(); LoadingScreen("Loading the Game", "Setup game variables", nil); CClock::Initialise(1000); CHeli::InitHelis(); @@ -227,13 +385,225 @@ bool CGame::Initialise(const char* datFile) CTheScripts::Process(); TheCamera.Process(); LoadingScreen("Loading the Game", "Load scene", nil); - CModelInfo::RemoveColModelsFromOtherLevels(CGame::currLevel); - CCollision::ms_collisionInMemory = CGame::currLevel; + CModelInfo::RemoveColModelsFromOtherLevels(currLevel); + CCollision::ms_collisionInMemory = currLevel; for (int i = 0; i < MAX_PADS; i++) CPad::GetPad(i)->Clear(true); return true; } +bool CGame::ShutDown(void) +{ + CReplay::FinishPlayback(); + CPlane::Shutdown(); + CTrain::Shutdown(); + CSpecialFX::Shutdown(); + CGarages::Shutdown(); + CMovingThings::Shutdown(); + gPhoneInfo.Shutdown(); + CWeapon::ShutdownWeapons(); + CPedType::Shutdown(); + CMBlur::MotionBlurClose(); + + for (int32 i = 0; i < NUMPLAYERS; i++) + { + if ( CWorld::Players[i].m_pPed ) + { + CWorld::Remove(CWorld::Players[i].m_pPed); + delete CWorld::Players[i].m_pPed; + CWorld::Players[i].m_pPed = NULL; + } + + CWorld::Players[i].Clear(); + } + + CRenderer::Shutdown(); + CWorld::ShutDown(); + DMAudio.DestroyAllGameCreatedEntities(); + CModelInfo::ShutDown(); + CAnimManager::Shutdown(); + CCutsceneMgr::Shutdown(); + CVehicleModelInfo::DeleteVehicleColourTextures(); + CVehicleModelInfo::ShutdownEnvironmentMaps(); + CRadar::Shutdown(); + CStreaming::Shutdown(); + CTxdStore::GameShutdown(); + CCollision::Shutdown(); + CWaterLevel::Shutdown(); + CRubbish::Shutdown(); + CClouds::Shutdown(); + CShadows::Shutdown(); + CCoronas::Shutdown(); + CSkidmarks::Shutdown(); + CWeaponEffects::Shutdown(); + CParticle::Shutdown(); + CPools::ShutDown(); + CTxdStore::RemoveTxdSlot(gameTxdSlot); + CdStreamRemoveImages(); + return true; +} + +void CGame::ReInitGameObjectVariables(void) +{ + CGameLogic::InitAtStartOfGame(); + TheCamera.CCamera::Init(); + TheCamera.SetRwCamera(Scene.camera); + CDebug::DebugInitTextBuffer(); + CWeather::Init(); + CUserDisplay::Init(); + CMessages::Init(); + CRestart::Initialise(); + CWorld::bDoingCarCollisions = false; + CHud::ReInitialise(); + CRadar::Initialise(); + CCarCtrl::ReInit(); + CTimeCycle::Initialise(); + CDraw::SetFOV(120.0f); + CDraw::ms_fLODDistance = 500.0f; + CStreaming::RequestBigBuildings(LEVEL_NONE); + CStreaming::LoadAllRequestedModels(false); + CPed::Initialise(); + CEventList::Initialise(); + CWeapon::InitialiseWeapons(); + CPopulation::Initialise(); + + for (int i = 0; i < NUMPLAYERS; i++) + CWorld::Players[i].Clear(); + + CWorld::PlayerInFocus = 0; + CAntennas::Init(); + CGlass::Init(); + gPhoneInfo.Initialise(); + CTheScripts::Init(); + CGangs::Initialise(); + CTimer::Initialise(); + CClock::Initialise(1000); + CTheCarGenerators::Init(); + CHeli::InitHelis(); + CMovingThings::Init(); + CDarkel::Init(); + CStats::Init(); + CPickups::Init(); + CPacManPickups::Init(); + CGarages::Init(); + CSpecialFX::Init(); + CWaterCannons::Init(); + CParticle::ReloadConfig(); + CCullZones::ResolveVisibilities(); + + if ( !FrontEndMenuManager.m_bLoadingSavedGame ) + { + CCranes::InitCranes(); + CTheScripts::StartTestScript(); + CTheScripts::Process(); + TheCamera.Process(); + CTrain::InitTrains(); + CPlane::InitPlanes(); + } + + for (int32 i = 0; i < MAX_PADS; i++) + CPad::GetPad(i)->Clear(true); +} + +void CGame::ReloadIPLs(void) +{ + CTimer::Stop(); + CWorld::RemoveStaticObjects(); + ThePaths.Init(); + CCullZones::Init(); + CFileLoader::ReloadPaths("GTA3.IDE"); + CFileLoader::LoadScene("INDUST.IPL"); + CFileLoader::LoadScene("COMMER.IPL"); + CFileLoader::LoadScene("SUBURBAN.IPL"); + CFileLoader::LoadScene("CULL.IPL"); + ThePaths.PreparePathData(); + CTrafficLights::ScanForLightsOnMap(); + CRoadBlocks::Init(); + CCranes::InitCranes(); + CGarages::Init(); + CWorld::RepositionCertainDynamicObjects(); + CCullZones::ResolveVisibilities(); + CRenderer::SortBIGBuildings(); + CTimer::Update(); +} + +void CGame::ShutDownForRestart(void) +{ + CReplay::FinishPlayback(); + CReplay::EmptyReplayBuffer(); + DMAudio.DestroyAllGameCreatedEntities(); + + for (int i = 0; i < NUMPLAYERS; i++) + CWorld::Players[i].Clear(); + + CGarages::SetAllDoorsBackToOriginalHeight(); + CTheScripts::UndoBuildingSwaps(); + CTheScripts::UndoEntityInvisibilitySettings(); + CWorld::ClearForRestart(); + CTimer::Shutdown(); + CStreaming::FlushRequestList(); + CStreaming::DeleteAllRwObjects(); + CStreaming::RemoveAllUnusedModels(); + CStreaming::ms_disableStreaming = false; + CRadar::RemoveRadarSections(); + FrontEndMenuManager.UnloadTextures(); + CParticleObject::RemoveAllParticleObjects(); + CPedType::Shutdown(); + CSpecialFX::Shutdown(); + TidyUpMemory(true, false); +} + +void CGame::InitialiseWhenRestarting(void) +{ + CRect rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + CRGBA color(255, 255, 255, 255); + + CTimer::Initialise(); + CSprite2d::SetRecipNearClip(); + + b_FoundRecentSavedGameWantToLoad = false; + + TheCamera.Init(); + + if ( FrontEndMenuManager.m_bLoadingSavedGame == true ) + { + RestoreForStartLoad(); + CStreaming::LoadScene(TheCamera.GetPosition()); + } + + ReInitGameObjectVariables(); + + if ( FrontEndMenuManager.m_bLoadingSavedGame == true ) + { + if ( GenericLoad() == true ) + { + DMAudio.ResetTimers(CTimer::GetTimeInMilliseconds()); + CTrain::InitTrains(); + CPlane::InitPlanes(); + } + else + { + for ( int32 i = 0; i < 50; i++ ) + { + HandleExit(); + FrontEndMenuManager.MessageScreen("FED_LFL"); // Loading save game has failed. The game will restart now. + } + + ShutDownForRestart(); + CTimer::Stop(); + CTimer::Initialise(); + FrontEndMenuManager.m_bLoadingSavedGame = false; + ReInitGameObjectVariables(); + currLevel = LEVEL_INDUSTRIAL; + CCollision::SortOutCollisionAfterLoad(); + } + } + + CTimer::Update(); + + DMAudio.ChangeMusicMode(MUSICMODE_GAME); +} + #if 0 WRAPPER void CGame::Process(void) { EAXJMP(0x48C850); } #else @@ -314,48 +684,33 @@ void CGame::Process(void) } #endif -void CGame::ReloadIPLs(void) +void CGame::DrasticTidyUpMemory(bool) { - CTimer::Stop(); - CWorld::RemoveStaticObjects(); - ThePaths.Init(); - CCullZones::Init(); - CFileLoader::ReloadPaths("GTA3.IDE"); - CFileLoader::LoadScene("INDUST.IPL"); - CFileLoader::LoadScene("COMMER.IPL"); - CFileLoader::LoadScene("SUBURBAN.IPL"); - CFileLoader::LoadScene("CULL.IPL"); - ThePaths.PreparePathData(); - CTrafficLights::ScanForLightsOnMap(); - CRoadBlocks::Init(); - CCranes::InitCranes(); - CGarages::Init(); - CWorld::RepositionCertainDynamicObjects(); - CCullZones::ResolveVisibilities(); - CRenderer::SortBIGBuildings(); - CTimer::Update(); -} - -#if 0 -WRAPPER void CGame::FinalShutdown(void) { EAXJMP(0x48BEC0); } -#else -void -CGame::FinalShutdown(void) -{ - CTxdStore::Shutdown(); - CPedStats::Shutdown(); - CdStreamShutdown(); -} +#ifdef PS2 + // meow #endif +} -WRAPPER bool CGame::InitialiseRenderWare(void) { EAXJMP(0x48BBA0); } -WRAPPER void CGame::ShutdownRenderWare(void) { EAXJMP(0x48BCB0); } -WRAPPER void CGame::ShutDown(void) { EAXJMP(0x48C3A0); } -WRAPPER void CGame::ShutDownForRestart(void) { EAXJMP(0x48C6B0); } -WRAPPER void CGame::InitialiseWhenRestarting(void) { EAXJMP(0x48C740); } -WRAPPER bool CGame::InitialiseOnceAfterRW(void) { EAXJMP(0x48BD50); } +void CGame::TidyUpMemory(bool, bool) +{ +#ifdef PS2 + // meow +#endif +} STARTPATCHES - InjectHook(0x48C850, CGame::Process, PATCH_JUMP); + InjectHook(0x48BB80, CGame::InitialiseOnceBeforeRW, PATCH_JUMP); + InjectHook(0x48BBA0, CGame::InitialiseRenderWare, PATCH_JUMP); + InjectHook(0x48BCB0, CGame::ShutdownRenderWare, PATCH_JUMP); + InjectHook(0x48BD50, CGame::InitialiseOnceAfterRW, PATCH_JUMP); InjectHook(0x48BEC0, CGame::FinalShutdown, PATCH_JUMP); + InjectHook(0x48BED0, CGame::Initialise, PATCH_JUMP); + InjectHook(0x48C3A0, CGame::ShutDown, PATCH_JUMP); + InjectHook(0x48C4B0, CGame::ReInitGameObjectVariables, PATCH_JUMP); + InjectHook(0x48C620, CGame::ReloadIPLs, PATCH_JUMP); + InjectHook(0x48C6B0, CGame::ShutDownForRestart, PATCH_JUMP); + InjectHook(0x48C740, CGame::InitialiseWhenRestarting, PATCH_JUMP); + InjectHook(0x48C850, CGame::Process, PATCH_JUMP); + InjectHook(0x48CA10, CGame::DrasticTidyUpMemory, PATCH_JUMP); + InjectHook(0x48CA20, CGame::TidyUpMemory, PATCH_JUMP); ENDPATCHES diff --git a/src/core/Game.h b/src/core/Game.h index 7b20099c..b6728a2f 100644 --- a/src/core/Game.h +++ b/src/core/Game.h @@ -20,19 +20,20 @@ public: static bool &playingIntro; static char *aDatFile; //[32]; - static bool Initialise(const char *datFile); static bool InitialiseOnceBeforeRW(void); static bool InitialiseRenderWare(void); - static bool InitialiseOnceAfterRW(void); - static void InitialiseWhenRestarting(void); - static void ShutDown(void); static void ShutdownRenderWare(void); + static bool InitialiseOnceAfterRW(void); static void FinalShutdown(void); - static void ShutDownForRestart(void); - static void Process(void); + static bool Initialise(const char *datFile); + static bool ShutDown(void); + static void ReInitGameObjectVariables(void); static void ReloadIPLs(void); - + static void ShutDownForRestart(void); + static void InitialiseWhenRestarting(void); + static void Process(void); + // NB: these do something on PS2 - static void TidyUpMemory(bool, bool) {} - static void DrasticTidyUpMemory(void) {} + static void TidyUpMemory(bool, bool); + static void DrasticTidyUpMemory(bool); }; diff --git a/src/core/RwHelper.cpp b/src/core/RwHelper.cpp index 1030d69e..6325bf15 100644 --- a/src/core/RwHelper.cpp +++ b/src/core/RwHelper.cpp @@ -352,7 +352,25 @@ WRAPPER bool CheckVideoCardCaps(void) { EAXJMP(0x592740); } WRAPPER void WriteVideoCardCapsFile(void) { EAXJMP(0x5927D0); } WRAPPER void ConvertingTexturesScreen(uint32, uint32, const char*) { EAXJMP(0x592880); } WRAPPER void DealWithTxdWriteError(uint32, uint32, const char*) { EAXJMP(0x592BF0); } -WRAPPER bool ConvertTextures() { EAXJMP(0x592C70); } +WRAPPER bool CreateTxdImageForVideoCard() { EAXJMP(0x592C70); } + +void CreateDebugFont() +{ + ; +} + +void DestroyDebugFont() +{ + ; +} + +void FlushObrsPrintfs() +{ + ; +} + +WRAPPER void _TexturePoolsInitialise() { EAXJMP(0x598B10); } +WRAPPER void _TexturePoolsShutdown() { EAXJMP(0x598B30); } STARTPATCHES //InjectHook(0x526450, GetFirstObjectCallback, PATCH_JUMP); diff --git a/src/core/RwHelper.h b/src/core/RwHelper.h index 1f0290cc..a9f0bdf4 100644 --- a/src/core/RwHelper.h +++ b/src/core/RwHelper.h @@ -3,6 +3,9 @@ void *RwMallocAlign(RwUInt32 size, RwUInt32 align); void RwFreeAlign(void *mem); +void CreateDebugFont(); +void DestroyDebugFont(); +void FlushObrsPrintfs(); void DefinedState(void); RwFrame *GetFirstChild(RwFrame *frame); RwObject *GetFirstObject(RwFrame *frame); @@ -17,7 +20,7 @@ bool CheckVideoCardCaps(void); void WriteVideoCardCapsFile(void); void ConvertingTexturesScreen(uint32, uint32, const char*); void DealWithTxdWriteError(uint32, uint32, const char*); -bool ConvertTextures(); // not a real name +bool CreateTxdImageForVideoCard(); bool RpClumpGtaStreamRead1(RwStream *stream); RpClump *RpClumpGtaStreamRead2(RwStream *stream); @@ -31,3 +34,7 @@ void CameraDestroy(RwCamera *camera); RwCamera *CameraCreate(RwInt32 width, RwInt32 height, RwBool zBuffer); + + +void _TexturePoolsInitialise(); +void _TexturePoolsShutdown(); \ No newline at end of file diff --git a/src/core/TxdStore.cpp b/src/core/TxdStore.cpp index ab970b99..c751147d 100644 --- a/src/core/TxdStore.cpp +++ b/src/core/TxdStore.cpp @@ -10,7 +10,7 @@ CPool *&CTxdStore::ms_pTxdPool = *(CPool**)0x8F5FB RwTexDictionary *&CTxdStore::ms_pStoredTxd = *(RwTexDictionary**)0x9405BC; void -CTxdStore::Initialize(void) +CTxdStore::Initialise(void) { if(ms_pTxdPool == nil) ms_pTxdPool = new CPool(TXDSTORESIZE); @@ -187,7 +187,7 @@ CTxdStore::RemoveTxd(int slot) } STARTPATCHES - InjectHook(0x527440, CTxdStore::Initialize, PATCH_JUMP); + InjectHook(0x527440, CTxdStore::Initialise, PATCH_JUMP); InjectHook(0x527470, CTxdStore::Shutdown, PATCH_JUMP); InjectHook(0x527490, CTxdStore::GameShutdown, PATCH_JUMP); InjectHook(0x5274E0, CTxdStore::AddTxdSlot, PATCH_JUMP); diff --git a/src/core/TxdStore.h b/src/core/TxdStore.h index a9e57d31..12ac708f 100644 --- a/src/core/TxdStore.h +++ b/src/core/TxdStore.h @@ -13,7 +13,7 @@ class CTxdStore static CPool *&ms_pTxdPool; static RwTexDictionary *&ms_pStoredTxd; public: - static void Initialize(void); + static void Initialise(void); static void Shutdown(void); static void GameShutdown(void); static int AddTxdSlot(const char *name); diff --git a/src/core/World.cpp b/src/core/World.cpp index 1dda1056..045a0bf3 100644 --- a/src/core/World.cpp +++ b/src/core/World.cpp @@ -39,6 +39,7 @@ bool &CWorld::bProcessCutsceneOnly = *(bool*)0x95CD8B; bool &CWorld::bDoingCarCollisions = *(bool*)0x95CD8C; bool &CWorld::bIncludeCarTyres = *(bool*)0x95CDAA; +WRAPPER void CWorld::ClearForRestart(void) { EAXJMP(0x4AE850); } WRAPPER void CWorld::AddParticles(void) { EAXJMP(0x4B4010); } WRAPPER void CWorld::ShutDown(void) { EAXJMP(0x4AE450); } WRAPPER void CWorld::RepositionCertainDynamicObjects() { EAXJMP(0x4B42B0); } diff --git a/src/core/World.h b/src/core/World.h index 4b19e629..461c72e6 100644 --- a/src/core/World.h +++ b/src/core/World.h @@ -134,6 +134,7 @@ public: static void Initialise(); static void AddParticles(); static void ShutDown(); + static void ClearForRestart(void); static void RepositionCertainDynamicObjects(); static void RemoveStaticObjects(); static void Process(); diff --git a/src/core/main.cpp b/src/core/main.cpp index 663b09da..50543b1e 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -90,7 +90,6 @@ void DoFade(void); void Render2dStuffAfterFade(void); CSprite2d *LoadSplash(const char *name); -void DestroySplashScreen(void); extern void (*DebugMenuProcess)(void); @@ -327,7 +326,7 @@ DoRWStuffEndOfFrame(void) { CDebug::DisplayScreenStrings(); // custom CDebug::DebugDisplayTextBuffer(); - // FlushObrsPrintfs(); + FlushObrsPrintfs(); RwCameraEndUpdate(Scene.camera); RsCameraShowRaster(Scene.camera); } diff --git a/src/core/main.h b/src/core/main.h index 570189b3..5e9401b9 100644 --- a/src/core/main.h +++ b/src/core/main.h @@ -28,6 +28,7 @@ void InitialiseGame(void); void LoadingScreen(const char *str1, const char *str2, const char *splashscreen); void LoadingIslandScreen(const char *levelName); CSprite2d *LoadSplash(const char *name); +void DestroySplashScreen(void); char *GetLevelSplashScreen(int level); char *GetRandomSplashScreen(void); void LittleTest(void); diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index 8b83d976..f43feae5 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -3619,11 +3619,11 @@ CPed::InflictDamage(CEntity *damagedBy, eWeaponType method, float damage, ePedPi if (DyingOrDead()) return false; - if (!bUsesCollision && method != WEAPONTYPE_WATER) + if (!bUsesCollision && method != WEAPONTYPE_DROWNING) return false; if (bOnlyDamagedByPlayer && damagedBy != player && damagedBy != FindPlayerVehicle() && - method != WEAPONTYPE_WATER && method != WEAPONTYPE_EXPLOSION) + method != WEAPONTYPE_DROWNING && method != WEAPONTYPE_EXPLOSION) return false; float healthImpact; @@ -3969,10 +3969,10 @@ CPed::InflictDamage(CEntity *damagedBy, eWeaponType method, float damage, ePedPi } } break; - case WEAPONTYPE_WATER: + case WEAPONTYPE_DROWNING: dieAnim = ANIM_DROWN; break; - case WEAPONTYPE_FALL_DAMAGE: + case WEAPONTYPE_FALL: if (bCollisionProof) return false; @@ -3998,7 +3998,7 @@ CPed::InflictDamage(CEntity *damagedBy, eWeaponType method, float damage, ePedPi } } - if (m_fArmour != 0.0f && method != WEAPONTYPE_WATER) { + if (m_fArmour != 0.0f && method != WEAPONTYPE_DROWNING) { if (player == this) CWorld::Players[CWorld::PlayerInFocus].m_nTimeLastArmourLoss = CTimer::GetTimeInMilliseconds(); @@ -4024,7 +4024,7 @@ CPed::InflictDamage(CEntity *damagedBy, eWeaponType method, float damage, ePedPi } if (bInVehicle) { - if (method != WEAPONTYPE_WATER) { + if (method != WEAPONTYPE_DROWNING) { #ifdef VC_PED_PORTS if (m_pMyVehicle) { if (m_pMyVehicle->IsCar() && m_pMyVehicle->pDriver == this) { @@ -4091,7 +4091,7 @@ CPed::InflictDamage(CEntity *damagedBy, eWeaponType method, float damage, ePedPi } else { CDarkel::RegisterKillNotByPlayer(this, method); } - if (method == WEAPONTYPE_WATER) + if (method == WEAPONTYPE_DROWNING) bIsInTheAir = false; return true; @@ -14575,7 +14575,7 @@ CPed::ProcessEntityCollision(CEntity *collidingEnt, CColPoint *collidingPoints) || m_pCollidingEntity == collidingEnt) { if (RpAnimBlendClumpGetAssociation(GetClump(), ANIM_FALL_FALL) && -0.016f * CTimer::GetTimeStep() > m_vecMoveSpeed.z) { - InflictDamage(collidingEnt, WEAPONTYPE_FALL_DAMAGE, 15.0f, PEDPIECE_TORSO, 2); + InflictDamage(collidingEnt, WEAPONTYPE_FALL, 15.0f, PEDPIECE_TORSO, 2); } } else { float damage = 100.0f * max(speed - 0.25f, 0.0f); @@ -14588,7 +14588,7 @@ CPed::ProcessEntityCollision(CEntity *collidingEnt, CColPoint *collidingPoints) CVector2D offset = -m_vecMoveSpeed; dir = GetLocalDirection(offset); } - InflictDamage(collidingEnt, WEAPONTYPE_FALL_DAMAGE, damage, PEDPIECE_TORSO, dir); + InflictDamage(collidingEnt, WEAPONTYPE_FALL, damage, PEDPIECE_TORSO, dir); if (IsPlayer() && damage2 > 5.0f) Say(SOUND_PED_LAND); } @@ -14599,7 +14599,7 @@ CPed::ProcessEntityCollision(CEntity *collidingEnt, CColPoint *collidingPoints) if (m_vecMoveSpeed.z >= -0.25f && (speedSqr = m_vecMoveSpeed.MagnitudeSqr()) <= sq(0.5f)) { if (RpAnimBlendClumpGetAssociation(GetClump(), ANIM_FALL_FALL) && -0.016f * CTimer::GetTimeStep() > m_vecMoveSpeed.z) { - InflictDamage(collidingEnt, WEAPONTYPE_FALL_DAMAGE, 15.0f, PEDPIECE_TORSO, 2); + InflictDamage(collidingEnt, WEAPONTYPE_FALL, 15.0f, PEDPIECE_TORSO, 2); } } else { if (speedSqr == 0.0f) @@ -14610,7 +14610,7 @@ CPed::ProcessEntityCollision(CEntity *collidingEnt, CColPoint *collidingPoints) CVector2D offset = -m_vecMoveSpeed; dir = GetLocalDirection(offset); } - InflictDamage(collidingEnt, WEAPONTYPE_FALL_DAMAGE, 350.0f * sq(speedSqr), PEDPIECE_TORSO, dir); + InflictDamage(collidingEnt, WEAPONTYPE_FALL, 350.0f * sq(speedSqr), PEDPIECE_TORSO, dir); } } #endif @@ -15036,7 +15036,7 @@ CPed::ProcessBuoyancy(void) CVector pos = GetPosition(); if (PlacePedOnDryLand()) { if (m_fHealth > 20.0f) - InflictDamage(nil, WEAPONTYPE_WATER, 15.0f, PEDPIECE_TORSO, false); + InflictDamage(nil, WEAPONTYPE_DROWNING, 15.0f, PEDPIECE_TORSO, false); if (bIsInTheAir) { RpAnimBlendClumpSetBlendDeltas(GetClump(), ASSOC_PARTIAL, -1000.0f); @@ -15058,7 +15058,7 @@ CPed::ProcessBuoyancy(void) m_vecMoveSpeed.y *= speedMult; m_vecMoveSpeed.z *= speedMult; bIsStanding = false; - InflictDamage(nil, WEAPONTYPE_WATER, 3.0f * CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); + InflictDamage(nil, WEAPONTYPE_DROWNING, 3.0f * CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); } if (buoyancyImpulse.z / m_fMass > 0.002f * CTimer::GetTimeStep()) { if (speedMult == 0.0f) { diff --git a/src/render/Rubbish.cpp b/src/render/Rubbish.cpp index c336eb47..a52e59a0 100644 --- a/src/render/Rubbish.cpp +++ b/src/render/Rubbish.cpp @@ -7,3 +7,4 @@ WRAPPER void CRubbish::StirUp(CVehicle *veh) { EAXJMP(0x512690); } WRAPPER void CRubbish::Update(void) { EAXJMP(0x511B90); } WRAPPER void CRubbish::SetVisibility(bool) { EAXJMP(0x512AA0); } WRAPPER void CRubbish::Init(void) { EAXJMP(0x511940); } +WRAPPER void CRubbish::Shutdown(void) { EAXJMP(0x511B50); } diff --git a/src/render/Rubbish.h b/src/render/Rubbish.h index c94ff303..17323694 100644 --- a/src/render/Rubbish.h +++ b/src/render/Rubbish.h @@ -10,4 +10,5 @@ public: static void Update(void); static void SetVisibility(bool); static void Init(void); + static void Shutdown(void); }; diff --git a/src/render/Skidmarks.cpp b/src/render/Skidmarks.cpp index deb5a648..c2725ed6 100644 --- a/src/render/Skidmarks.cpp +++ b/src/render/Skidmarks.cpp @@ -9,3 +9,4 @@ WRAPPER void CSkidmarks::Render(void) { EAXJMP(0x5182E0); } WRAPPER void CSkidmarks::RegisterOne(uint32 id, CVector pos, float fwdx, float fwdY, bool *isMuddy, bool *isBloddy) { EAXJMP(0x5185C0); } WRAPPER void CSkidmarks::Init(void) { EAXJMP(0x517D70); } +WRAPPER void CSkidmarks::Shutdown(void) { EAXJMP(0x518100); } diff --git a/src/render/Skidmarks.h b/src/render/Skidmarks.h index 2f669575..bf2da7e4 100644 --- a/src/render/Skidmarks.h +++ b/src/render/Skidmarks.h @@ -8,4 +8,5 @@ public: static void Render(void); static void RegisterOne(uint32 id, CVector pos, float fwdx, float fwdY, bool *isMuddy, bool *isBloddy); static void Init(void); + static void Shutdown(void); }; diff --git a/src/render/SpecialFX.cpp b/src/render/SpecialFX.cpp index 8ec2d9a1..301ae265 100644 --- a/src/render/SpecialFX.cpp +++ b/src/render/SpecialFX.cpp @@ -20,6 +20,7 @@ WRAPPER void CSpecialFX::Render(void) { EAXJMP(0x518DC0); } WRAPPER void CSpecialFX::Update(void) { EAXJMP(0x518D40); } WRAPPER void CSpecialFX::Init(void) { EAXJMP(0x5189E0); } +WRAPPER void CSpecialFX::Shutdown(void) { EAXJMP(0x518BE0); } WRAPPER void CMotionBlurStreaks::RegisterStreak(int32 id, uint8 r, uint8 g, uint8 b, CVector p1, CVector p2) { EAXJMP(0x519460); } diff --git a/src/render/SpecialFX.h b/src/render/SpecialFX.h index 701b89a0..fc155a53 100644 --- a/src/render/SpecialFX.h +++ b/src/render/SpecialFX.h @@ -6,6 +6,7 @@ public: static void Render(void); static void Update(void); static void Init(void); + static void Shutdown(void); }; class CMotionBlurStreaks diff --git a/src/render/WeaponEffects.cpp b/src/render/WeaponEffects.cpp index 932c661e..1c29caf8 100644 --- a/src/render/WeaponEffects.cpp +++ b/src/render/WeaponEffects.cpp @@ -1,45 +1,106 @@ #include "common.h" #include "patcher.h" #include "WeaponEffects.h" - #include "TxdStore.h" +#include "Sprite.h" -WRAPPER void CWeaponEffects::Render(void) { EAXJMP(0x564D70); } +RwTexture *gpCrossHairTex; +RwRaster *gpCrossHairRaster; -CWeaponEffects &gCrossHair = *(CWeaponEffects*)0x6503BC; +CWeaponEffects gCrossHair; + +CWeaponEffects::CWeaponEffects() +{ + +} + +CWeaponEffects::~CWeaponEffects() +{ + +} void -CWeaponEffects::ClearCrossHair() +CWeaponEffects::Init(void) { - gCrossHair.m_bCrosshair = false; + gCrossHair.m_bActive = false; + gCrossHair.m_vecPos = CVector(0.0f, 0.0f, 0.0f); + gCrossHair.m_nRed = 0; + gCrossHair.m_nGreen = 0; + gCrossHair.m_nBlue = 0; + gCrossHair.m_nAlpha = 255; + gCrossHair.m_fSize = 1.0f; + gCrossHair.m_fRotation = 0.0f; + + + CTxdStore::PushCurrentTxd(); + int32 slut = CTxdStore::FindTxdSlot("particle"); + CTxdStore::SetCurrentTxd(slut); + + gpCrossHairTex = RwTextureRead("crosshair", NULL); + gpCrossHairRaster = RwTextureGetRaster(gpCrossHairTex); + + CTxdStore::PopCurrentTxd(); +} + +void +CWeaponEffects::Shutdown(void) +{ + RwTextureDestroy(gpCrossHairTex); } void CWeaponEffects::MarkTarget(CVector pos, uint8 red, uint8 green, uint8 blue, uint8 alpha, float size) { - gCrossHair.m_bCrosshair = true; + gCrossHair.m_bActive = true; gCrossHair.m_vecPos = pos; - gCrossHair.m_red = red; - gCrossHair.m_green = green; - gCrossHair.m_blue = blue; - gCrossHair.m_alpha = alpha; - gCrossHair.m_size = size; + gCrossHair.m_nRed = red; + gCrossHair.m_nGreen = green; + gCrossHair.m_nBlue = blue; + gCrossHair.m_nAlpha = alpha; + gCrossHair.m_fSize = size; } void -CWeaponEffects::Init() +CWeaponEffects::ClearCrossHair(void) { - gCrossHair.m_bCrosshair = false; - gCrossHair.m_vecPos = CVector(0.0f, 0.0f, 0.0f); - gCrossHair.m_red = 0; - gCrossHair.m_green = 0; - gCrossHair.m_blue = 0; - gCrossHair.m_alpha = 255; - gCrossHair.m_size = 1.0f; - gCrossHair.field_24 = 0; - CTxdStore::PushCurrentTxd(); - CTxdStore::SetCurrentTxd(CTxdStore::FindTxdSlot("particle")); - gCrossHair.m_pTexture = RwTextureRead("crosshair", nil); - gCrossHair.m_pRaster = gCrossHair.m_pTexture->raster; - CTxdStore::PopCurrentTxd(); + gCrossHair.m_bActive = false; } + +void +CWeaponEffects::Render(void) +{ + if ( gCrossHair.m_bActive ) + { + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)gpCrossHairRaster); + + RwV3d pos; + float w, h; + if ( CSprite::CalcScreenCoors(gCrossHair.m_vecPos, &pos, &w, &h, true) ) + { + float recipz = 1.0f / pos.z; + CSprite::RenderOneXLUSprite(pos.x, pos.y, pos.z, + gCrossHair.m_fSize * w, gCrossHair.m_fSize * h, + gCrossHair.m_nRed, gCrossHair.m_nGreen, gCrossHair.m_nBlue, 255, + recipz, 255); + } + + RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDSRCALPHA); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDINVSRCALPHA); + } +} + +STARTPATCHES + //InjectHook(0x564C40, CWeaponEffects::CWeaponEffects, PATCH_JUMP); + //InjectHook(0x564C50, CWeaponEffects::~CWeaponEffects, PATCH_JUMP); + InjectHook(0x564C60, CWeaponEffects::Init, PATCH_JUMP); + InjectHook(0x564CF0, CWeaponEffects::Shutdown, PATCH_JUMP); + InjectHook(0x564D00, CWeaponEffects::MarkTarget, PATCH_JUMP); + InjectHook(0x564D60, CWeaponEffects::ClearCrossHair, PATCH_JUMP); + InjectHook(0x564D70, CWeaponEffects::Render, PATCH_JUMP); +ENDPATCHES \ No newline at end of file diff --git a/src/render/WeaponEffects.h b/src/render/WeaponEffects.h index e4d0461a..31c5a309 100644 --- a/src/render/WeaponEffects.h +++ b/src/render/WeaponEffects.h @@ -3,21 +3,25 @@ class CWeaponEffects { public: - bool m_bCrosshair; - int8 gap_1[3]; + bool m_bActive; + char _pad[3]; CVector m_vecPos; - uint8 m_red; - uint8 m_green; - uint8 m_blue; - uint8 m_alpha; - float m_size; - int32 field_24; - RwTexture *m_pTexture; - RwRaster *m_pRaster; + uint8 m_nRed; + uint8 m_nGreen; + uint8 m_nBlue; + uint8 m_nAlpha; + float m_fSize; + float m_fRotation; public: - static void Render(void); - static void ClearCrossHair(); - static void MarkTarget(CVector, uint8, uint8, uint8, uint8, float); + CWeaponEffects(); + ~CWeaponEffects(); + static void Init(void); + static void Shutdown(void); + static void MarkTarget(CVector pos, uint8 red, uint8 green, uint8 blue, uint8 alpha, float size); + static void ClearCrossHair(void); + static void Render(void); }; + +VALIDATE_SIZE(CWeaponEffects, 0x1C); \ No newline at end of file diff --git a/src/vehicles/Automobile.cpp b/src/vehicles/Automobile.cpp index 8cb0cfa4..e709a87f 100644 --- a/src/vehicles/Automobile.cpp +++ b/src/vehicles/Automobile.cpp @@ -2823,13 +2823,13 @@ CAutomobile::ProcessBuoyancy(void) if(pDriver){ pDriver->bIsInWater = true; if(pDriver->IsPlayer() || !bWaterTight) - pDriver->InflictDamage(nil, WEAPONTYPE_WATER, CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); + pDriver->InflictDamage(nil, WEAPONTYPE_DROWNING, CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); } for(i = 0; i < m_nNumMaxPassengers; i++) if(pPassengers[i]){ pPassengers[i]->bIsInWater = true; if(pPassengers[i]->IsPlayer() || !bWaterTight) - pPassengers[i]->InflictDamage(nil, WEAPONTYPE_WATER, CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); + pPassengers[i]->InflictDamage(nil, WEAPONTYPE_DROWNING, CTimer::GetTimeStep(), PEDPIECE_TORSO, 0); } }else bIsInWater = false; diff --git a/src/weapons/Weapon.cpp b/src/weapons/Weapon.cpp index 8019a1cf..09844c23 100644 --- a/src/weapons/Weapon.cpp +++ b/src/weapons/Weapon.cpp @@ -6,6 +6,7 @@ #include "Ped.h" #include "World.h" +WRAPPER void CWeapon::ShutdownWeapons(void) { EAXJMP(0x55C2F0); } WRAPPER void CWeapon::UpdateWeapons(void) { EAXJMP(0x55C310); } WRAPPER bool CWeapon::Fire(CEntity*, CVector*) { EAXJMP(0x55C380); } WRAPPER void CWeapon::FireFromCar(CAutomobile *car, bool left) { EAXJMP(0x55C940); } diff --git a/src/weapons/Weapon.h b/src/weapons/Weapon.h index 1db66720..74145564 100644 --- a/src/weapons/Weapon.h +++ b/src/weapons/Weapon.h @@ -15,17 +15,19 @@ enum eWeaponType WEAPONTYPE_MOLOTOV, WEAPONTYPE_GRENADE, WEAPONTYPE_DETONATOR, - WEAPONTYPE_TOTAL_INVENTORY_WEAPONS = 13, - WEAPONTYPE_HELICANNON = 13, - WEAPONTYPE_TOTALWEAPONS, + WEAPONTYPE_HELICANNON, + WEAPONTYPE_LAST_WEAPONTYPE, WEAPONTYPE_ARMOUR, WEAPONTYPE_RAMMEDBYCAR, WEAPONTYPE_RUNOVERBYCAR, WEAPONTYPE_EXPLOSION, WEAPONTYPE_UZI_DRIVEBY, - WEAPONTYPE_WATER, - WEAPONTYPE_FALL_DAMAGE, + WEAPONTYPE_DROWNING, + WEAPONTYPE_FALL, WEAPONTYPE_UNIDENTIFIED, + + WEAPONTYPE_TOTALWEAPONS = WEAPONTYPE_LAST_WEAPONTYPE, + WEAPONTYPE_TOTAL_INVENTORY_WEAPONS = 13, }; enum eWeaponFire { @@ -63,6 +65,7 @@ public: m_bAddRotOffset = false; } + static void ShutdownWeapons(void); void Initialise(eWeaponType type, int ammo); void Update(int32 audioEntity); void Reload(void); From 38c2f8fbb06b9fcbd64efb842f53944cb53bcda7 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sun, 29 Mar 2020 01:48:49 +0300 Subject: [PATCH 42/70] fixes --- src/control/Garages.cpp | 76 +++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 672c3381..95f3a83d 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -46,7 +46,7 @@ #define DISTANCE_TO_CLOSE_MISSION_GARAGE (30.0f) #define DISTANCE_TO_CLOSE_COLLECTSPECIFICCARS_GARAGE (25.0f) #define DISTANCE_TO_CLOSE_COLLECTCARS_GARAGE (40.0f) -#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT (2.4f) +#define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT (2.2f) #define DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR (15.0f) #define DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE (70.0f) #define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_ON_FOOT (1.7f) @@ -334,7 +334,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_RESPRAY; @@ -431,9 +431,9 @@ void CGarage::Update() m_fY2 + DISTANCE_TO_CALL_OFF_CHASE); break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { - m_eGarageState = GS_OPENED; + m_eGarageState = GS_OPENEDCONTAINSCAR; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); } UpdateDoorsHeight(); @@ -476,7 +476,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; m_nTimeToStartAction = CTimer::GetTimeInMilliseconds() + TIME_TO_SETUP_BOMB; @@ -532,12 +532,12 @@ void CGarage::Update() } CPad::GetPad(0)->SetEnablePlayerControls(PLAYERCONTROL_GARAGE); FindPlayerPed()->m_pWanted->m_bIgnoredByCops = false; - break; } + break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { - m_eGarageState = GS_OPENED; + m_eGarageState = GS_OPENEDCONTAINSCAR; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); } UpdateDoorsHeight(); @@ -556,20 +556,21 @@ void CGarage::Update() switch (m_eGarageState) { case GS_OPENED: if (((CVector2D)FindPlayerCoors() - CVector2D(GetGarageCenterX(), GetGarageCenterY())).MagnitudeSqr() > SQR(DISTANCE_TO_CLOSE_MISSION_GARAGE)) { - if (!FindPlayerVehicle() && m_pTarget && IsEntityEntirelyInside3D(m_pTarget, 0.0f) && !IsAnyOtherCarTouchingGarage(m_pTarget)) { - CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); - FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; + if ((CTimer::GetFrameCounter() & 0x1F) == 0 && !IsAnyOtherCarTouchingGarage(nil)) { m_eGarageState = GS_CLOSING; - m_bClosingWithoutTargetCar = false; + m_bClosingWithoutTargetCar = true; } } - else if ((CTimer::GetFrameCounter() & 0x1F) == 0 && IsAnyOtherCarTouchingGarage(nil)) { + else if (!FindPlayerVehicle() && m_pTarget && IsEntityEntirelyInside3D(m_pTarget, 0.0f) && + !IsAnyOtherCarTouchingGarage(m_pTarget) && IsEntityEntirelyOutside(FindPlayerPed(), 2.0f)) { + CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); + FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; m_eGarageState = GS_CLOSING; - m_bClosingWithoutTargetCar = true; + m_bClosingWithoutTargetCar = false; } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); if (m_bClosingWithoutTargetCar) @@ -598,7 +599,7 @@ void CGarage::Update() } break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -635,7 +636,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -682,7 +683,7 @@ void CGarage::Update() m_pTarget = FindPlayerVehicle(); m_pTarget->RegisterReference((CEntity**)&m_pTarget); } - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -731,7 +732,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -771,7 +772,7 @@ void CGarage::Update() m_pTarget = FindPlayerVehicle(); m_pTarget->RegisterReference((CEntity**)&m_pTarget); } - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -792,7 +793,7 @@ void CGarage::Update() m_eGarageState = GS_CLOSING; break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -803,16 +804,16 @@ void CGarage::Update() case GS_FULLYCLOSED: break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); } UpdateDoorsHeight(); break; - case GS_OPENEDCONTAINSCAR: - case GS_CLOSEDCONTAINSCAR: - case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -906,7 +907,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); if (m_bClosingWithoutTargetCar) @@ -934,7 +935,7 @@ void CGarage::Update() m_eGarageState = GS_OPENING; break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -954,7 +955,7 @@ void CGarage::Update() case GARAGE_FOR_SCRIPT_TO_OPEN: switch (m_eGarageState) { case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -974,7 +975,7 @@ void CGarage::Update() case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: switch (m_eGarageState) { case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -982,7 +983,7 @@ void CGarage::Update() UpdateDoorsHeight(); break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1027,9 +1028,9 @@ void CGarage::Update() #ifndef FIX_BUGS // TODO: check and replace with ifdef if (!IsPlayerOutsideGarage()) m_eGarageState = GS_OPENING; - m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); #else - m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (!IsPlayerOutsideGarage()) m_eGarageState = GS_OPENING; #endif @@ -1076,7 +1077,7 @@ void CGarage::Update() break; } case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1101,7 +1102,7 @@ void CGarage::Update() } break; case GS_CLOSING: - m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : ROTATED_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == 0.0f) { m_eGarageState = GS_FULLYCLOSED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); @@ -1117,7 +1118,7 @@ void CGarage::Update() m_eGarageState = GS_OPENING; break; case GS_OPENING: - m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : ROTATED_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); + m_fDoorPos = min(m_fDoorHeight, m_fDoorPos + (m_bRotatedDoor ? ROTATED_DOOR_OPEN_SPEED : DEFAULT_DOOR_OPEN_SPEED) * CTimer::GetTimeStep()); if (m_fDoorPos == m_fDoorHeight) { m_eGarageState = GS_OPENED; DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_OPENED, 1.0f); @@ -1244,10 +1245,6 @@ void CGarage::RefreshDoorPointers(bool bCreate) bNeedToFindDoorEntities = true; if (bNeedToFindDoorEntities) FindDoorsEntities(); - if (m_pDoor1 && bCreate) - debug("Created door 1 for type %d", m_eGarageType); - if (m_pDoor2 && bCreate) - debug("Created door 2 for type %d", m_eGarageType); } WRAPPER void CGarages::TriggerMessage(const char* text, int16, uint16 time, int16) { EAXJMP(0x426B20); } @@ -1412,5 +1409,4 @@ STARTPATCHES InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); #endif InjectHook(0x421E40, CGarages::Update, PATCH_JUMP); - InjectHook(0x4268A0, &CGarage::UpdateCrusherAngle, PATCH_JUMP); ENDPATCHES \ No newline at end of file From 02615ccd983e270fbd8ad3e9a98e9020756fc60e Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sun, 29 Mar 2020 02:09:02 +0300 Subject: [PATCH 43/70] fix --- src/control/Garages.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 95f3a83d..b4d7a703 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -167,7 +167,7 @@ void CGarages::Update(void) } if ((CTimer::GetFrameCounter() & 0xF) != 0xC) return; - if (++GarageToBeTidied >= 32) + if (++GarageToBeTidied >= NUM_GARAGES) GarageToBeTidied = 0; if (!aGarages[GarageToBeTidied].IsUsed()) return; From db92864fe2ac5fa6fda47d0db634bf466d060356 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Sun, 29 Mar 2020 08:51:30 +0300 Subject: [PATCH 44/70] Fire and PlayerSkin fix --- src/core/Fire.cpp | 8 +++-- src/core/PlayerSkin.cpp | 72 ++++++++++++++++++++--------------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/src/core/Fire.cpp b/src/core/Fire.cpp index c98c808d..cfa849e9 100644 --- a/src/core/Fire.cpp +++ b/src/core/Fire.cpp @@ -113,7 +113,7 @@ CFire::ProcessFire(void) CVector(0.0f, 0.0f, CGeneral::GetRandomNumberInRange(0.0125f, 0.1f) * m_fStrength), 0, m_fStrength, 0, 0, 0, 0); - rand(); rand(); rand(); /* unsure why these three rands are called */ + CGeneral::GetRandomNumber(); CGeneral::GetRandomNumber(); CGeneral::GetRandomNumber(); /* unsure why these three rands are called */ CParticle::AddParticle(PARTICLE_CARFLAME_SMOKE, firePos, CVector(0.0f, 0.0f, 0.0f), 0, 0.0f, 0, 0, 0, 0); @@ -129,8 +129,10 @@ CFire::ProcessFire(void) if (!m_pEntity) { CShadows::StoreStaticShadow((uint32)this, SHADOWTYPE_ADDITIVE, gpShadowExplosionTex, &lightpos, - 7.0f, 0.0f, 0.0f, -7.0f, 0, nRandNumber / 2, nRandNumber / 2, - 0, 10.0f, 1.0f, 40.0f, 0, 0.0f); + 7.0f, 0.0f, 0.0f, -7.0f, + 255, // this is 0 on PC which results in no shadow + nRandNumber / 2, nRandNumber / 2, 0, + 10.0f, 1.0f, 40.0f, 0, 0.0f); } fGreen = nRandNumber / 128; fRed = nRandNumber / 128; diff --git a/src/core/PlayerSkin.cpp b/src/core/PlayerSkin.cpp index 4d2c31df..4f730b90 100644 --- a/src/core/PlayerSkin.cpp +++ b/src/core/PlayerSkin.cpp @@ -1,22 +1,22 @@ -#include "common.h" -#include "patcher.h" -#include "main.h" -#include "PlayerSkin.h" -#include "TxdStore.h" -#include "rtbmp.h" -#include "ClumpModelInfo.h" -#include "VisibilityPlugins.h" -#include "World.h" -#include "PlayerInfo.h" -#include "CdStream.h" -#include "FileMgr.h" -#include "Directory.h" -#include "RwHelper.h" -#include "Timer.h" -#include "Lights.h" - -int CPlayerSkin::m_txdSlot; - +#include "common.h" +#include "patcher.h" +#include "main.h" +#include "PlayerSkin.h" +#include "TxdStore.h" +#include "rtbmp.h" +#include "ClumpModelInfo.h" +#include "VisibilityPlugins.h" +#include "World.h" +#include "PlayerInfo.h" +#include "CdStream.h" +#include "FileMgr.h" +#include "Directory.h" +#include "RwHelper.h" +#include "Timer.h" +#include "Lights.h" + +int CPlayerSkin::m_txdSlot; + void FindPlayerDff(uint32 &offset, uint32 &size) { @@ -32,8 +32,8 @@ FindPlayerDff(uint32 &offset, uint32 &size) offset = info.offset; size = info.size; -} - +} + void LoadPlayerDff(void) { @@ -65,22 +65,22 @@ LoadPlayerDff(void) if (streamWasAdded) CdStreamRemoveImages(); -} - +} + void CPlayerSkin::Initialise(void) { m_txdSlot = CTxdStore::AddTxdSlot("skin"); CTxdStore::Create(m_txdSlot); CTxdStore::AddRef(m_txdSlot); -} - +} + void CPlayerSkin::Shutdown(void) { CTxdStore::RemoveTxdSlot(m_txdSlot); -} - +} + RwTexture * CPlayerSkin::GetSkinTexture(const char *texName) { @@ -112,8 +112,8 @@ CPlayerSkin::GetSkinTexture(const char *texName) RwImageDestroy(image); } return tex; -} - +} + void CPlayerSkin::BeginFrontendSkinEdit(void) { @@ -163,11 +163,11 @@ CPlayerSkin::RenderFrontendSkinEdit(void) RpClumpRender(gpPlayerClump); } -STARTPATCHES -InjectHook(0x59B9B0, &CPlayerSkin::Initialise, PATCH_JUMP); -InjectHook(0x59B9E0, &CPlayerSkin::Shutdown, PATCH_JUMP); -InjectHook(0x59B9F0, &CPlayerSkin::GetSkinTexture, PATCH_JUMP); -InjectHook(0x59BC70, &CPlayerSkin::BeginFrontendSkinEdit, PATCH_JUMP); -InjectHook(0x59BCB0, &CPlayerSkin::EndFrontendSkinEdit, PATCH_JUMP); -InjectHook(0x59BCE0, &CPlayerSkin::RenderFrontendSkinEdit, PATCH_JUMP); +STARTPATCHES +InjectHook(0x59B9B0, &CPlayerSkin::Initialise, PATCH_JUMP); +InjectHook(0x59B9E0, &CPlayerSkin::Shutdown, PATCH_JUMP); +InjectHook(0x59B9F0, &CPlayerSkin::GetSkinTexture, PATCH_JUMP); +InjectHook(0x59BC70, &CPlayerSkin::BeginFrontendSkinEdit, PATCH_JUMP); +InjectHook(0x59BCB0, &CPlayerSkin::EndFrontendSkinEdit, PATCH_JUMP); +InjectHook(0x59BCE0, &CPlayerSkin::RenderFrontendSkinEdit, PATCH_JUMP); ENDPATCHES \ No newline at end of file From 97ffa1a6584fb9da20386dda6c171c00c937272b Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Sun, 29 Mar 2020 08:54:34 +0300 Subject: [PATCH 45/70] Wrappers cleanup --- src/control/AutoPilot.cpp | 4 - src/control/CarCtrl.cpp | 9 +- src/control/Curves.cpp | 10 +- src/control/Darkel.cpp | 16 --- src/control/Replay.cpp | 122 +--------------------- src/control/Script.cpp | 29 ------ src/core/Frontend.cpp | 4 - src/core/Game.cpp | 8 -- src/core/Radar.cpp | 174 -------------------------------- src/core/Timer.cpp | 4 - src/render/Coronas.cpp | 3 - src/render/Hud.cpp | 42 +------- src/save/GenericGameStorage.cpp | 1 - src/weapons/Explosion.cpp | 2 - 14 files changed, 5 insertions(+), 423 deletions(-) diff --git a/src/control/AutoPilot.cpp b/src/control/AutoPilot.cpp index e3d5c9e9..70099291 100644 --- a/src/control/AutoPilot.cpp +++ b/src/control/AutoPilot.cpp @@ -6,9 +6,6 @@ #include "Curves.h" #include "PathFind.h" -#if 0 -WRAPPER void CAutoPilot::ModifySpeed(float) { EAXJMP(0x4137B0); } -#else void CAutoPilot::ModifySpeed(float speed) { m_fMaxTrafficSpeed = max(0.01f, speed); @@ -41,7 +38,6 @@ void CAutoPilot::ModifySpeed(float speed) m_nTimeEnteredCurve = CTimer::GetTimeInMilliseconds() - positionBetweenNodes * m_nSpeedScaleFactor; #endif } -#endif void CAutoPilot::RemoveOnePathNode() { diff --git a/src/control/CarCtrl.cpp b/src/control/CarCtrl.cpp index 07ba2e3c..3174a253 100644 --- a/src/control/CarCtrl.cpp +++ b/src/control/CarCtrl.cpp @@ -881,9 +881,7 @@ CCarCtrl::SlowCarOnRailsDownForTrafficAndLights(CVehicle* pVehicle) pVehicle->AutoPilot.ModifySpeed(max(maxSpeed, curSpeed - 0.5f * CTimer::GetTimeStep())); } } -#if 0 -WRAPPER void CCarCtrl::SlowCarDownForPedsSectorList(CPtrList&, CVehicle*, float, float, float, float, float*, float) { EAXJMP(0x419300); } -#else + void CCarCtrl::SlowCarDownForPedsSectorList(CPtrList& lst, CVehicle* pVehicle, float x_inf, float y_inf, float x_sup, float y_sup, float* pSpeed, float curSpeed) { float frontOffset = pVehicle->GetModelInfo()->GetColModel()->boundingBox.max.y; @@ -991,7 +989,6 @@ void CCarCtrl::SlowCarDownForPedsSectorList(CPtrList& lst, CVehicle* pVehicle, f } } } -#endif void CCarCtrl::SlowCarDownForCarsSectorList(CPtrList& lst, CVehicle* pVehicle, float x_inf, float y_inf, float x_sup, float y_sup, float* pSpeed, float curSpeed) { @@ -1055,9 +1052,6 @@ void CCarCtrl::SlowCarDownForOtherCar(CEntity* pOtherEntity, CVehicle* pVehicle, } } -#if 0 -WRAPPER float CCarCtrl::TestCollisionBetween2MovingRects(CVehicle* pVehicleA, CVehicle* pVehicleB, float projectionX, float projectionY, CVector* pForwardA, CVector* pForwardB, uint8 id) { EAXJMP(0x41A020); } -#else float CCarCtrl::TestCollisionBetween2MovingRects(CVehicle* pVehicleA, CVehicle* pVehicleB, float projectionX, float projectionY, CVector* pForwardA, CVector* pForwardB, uint8 id) { CVector2D vecBToA = pVehicleA->GetPosition() - pVehicleB->GetPosition(); @@ -1180,7 +1174,6 @@ float CCarCtrl::TestCollisionBetween2MovingRects(CVehicle* pVehicleA, CVehicle* } return proximity; } -#endif float CCarCtrl::FindAngleToWeaveThroughTraffic(CVehicle* pVehicle, CPhysical* pTarget, float angleToTarget, float angleForward) { diff --git a/src/control/Curves.cpp b/src/control/Curves.cpp index c5f64bf7..5c6ef06d 100644 --- a/src/control/Curves.cpp +++ b/src/control/Curves.cpp @@ -2,9 +2,6 @@ #include "patcher.h" #include "Curves.h" -#if 0 -WRAPPER float CCurves::CalcSpeedScaleFactor(CVector*, CVector*, float, float, float, float) { EAXJMP(0x420410); } -#else float CCurves::CalcSpeedScaleFactor(CVector* pPoint1, CVector* pPoint2, float dir1X, float dir1Y, float dir2X, float dir2Y) { CVector2D dir1(dir1X, dir1Y); @@ -16,11 +13,7 @@ float CCurves::CalcSpeedScaleFactor(CVector* pPoint1, CVector* pPoint2, float di else return ((1.0f - dp) * 0.2f + 1.0f) * distance; } -#endif -#if 0 -WRAPPER void CCurves::CalcCurvePoint(CVector*, CVector*, CVector*, CVector*, float, int32, CVector*, CVector*) { EAXJMP(0x4204D0); } -#else void CCurves::CalcCurvePoint(CVector* pPos1, CVector* pPos2, CVector* pDir1, CVector* pDir2, float between, int32 timeOnCurve, CVector* pOutPos, CVector* pOutDir) { float actualFactor = CalcSpeedScaleFactor(pPos1, pPos2, pDir1->x, pDir1->y, pDir2->x, pDir2->y); @@ -35,5 +28,4 @@ void CCurves::CalcCurvePoint(CVector* pPos1, CVector* pPos2, CVector* pDir1, CVe (dir1.x * (1.0f - curveCoef) + dir2.x * curveCoef) / (timeOnCurve * 0.001f), (dir1.y * (1.0f - curveCoef) + dir2.y * curveCoef) / (timeOnCurve * 0.001f), 0.0f); -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/control/Darkel.cpp b/src/control/Darkel.cpp index ec1b887e..b4d15abf 100644 --- a/src/control/Darkel.cpp +++ b/src/control/Darkel.cpp @@ -162,9 +162,6 @@ CDarkel::ReadStatus() return Status; } -#if 0 -WRAPPER void CDarkel::RegisterCarBlownUpByPlayer(CVehicle *vehicle) { EAXJMP(0x421070); } -#else void CDarkel::RegisterCarBlownUpByPlayer(CVehicle *vehicle) { @@ -178,11 +175,7 @@ CDarkel::RegisterCarBlownUpByPlayer(CVehicle *vehicle) RegisteredKills[vehicle->GetModelIndex()]++; CStats::CarsExploded++; } -#endif -#if 0 -WRAPPER void CDarkel::RegisterKillByPlayer(CPed *victim, eWeaponType weapontype, bool headshot) { EAXJMP(0x420F60); } -#else void CDarkel::RegisterKillByPlayer(CPed *victim, eWeaponType weapon, bool headshot) { @@ -207,7 +200,6 @@ CDarkel::RegisterKillByPlayer(CPed *victim, eWeaponType weapon, bool headshot) CStats::HeadsPopped++; CStats::KillsSinceLastCheckpoint++; } -#endif void CDarkel::RegisterKillNotByPlayer(CPed* victim, eWeaponType weapontype) @@ -222,9 +214,6 @@ CDarkel::ResetModelsKilledByPlayer() RegisteredKills[i] = 0; } -#if 0 -WRAPPER void CDarkel::ResetOnPlayerDeath() { EAXJMP(0x420E70); } -#else void CDarkel::ResetOnPlayerDeath() { @@ -253,11 +242,7 @@ CDarkel::ResetOnPlayerDeath() player->MakeChangesForNewWeapon(player->m_currentWeapon); } } -#endif -#if 0 -WRAPPER void CDarkel::StartFrenzy(eWeaponType weaponType, int32 time, uint16 kill, int32 modelId0, wchar *text, int32 modelId2, int32 modelId3, int32 modelId4, bool standardSound, bool needHeadShot) { EAXJMP(0x4210E0); } -#else void CDarkel::StartFrenzy(eWeaponType weaponType, int32 time, uint16 kill, int32 modelId0, wchar *text, int32 modelId2, int32 modelId3, int32 modelId4, bool standardSound, bool needHeadShot) { @@ -306,7 +291,6 @@ CDarkel::StartFrenzy(eWeaponType weaponType, int32 time, uint16 kill, int32 mode if (CDarkel::bStandardSoundAndMessages) DMAudio.PlayFrontEndSound(SOUND_RAMPAGE_START, 0); } -#endif void CDarkel::Update() diff --git a/src/control/Replay.cpp b/src/control/Replay.cpp index 3c0393aa..2e325249 100644 --- a/src/control/Replay.cpp +++ b/src/control/Replay.cpp @@ -113,9 +113,6 @@ static void(*CBArray_RE3[])(CAnimBlendAssociation*, void*) = &CPed::PedLandCB, &FinishFuckUCB, &CPed::RestoreHeadingRateCB, &CPed::PedSetQuickDraggedOutCarPositionCB, &CPed::PedSetDraggedOutCarPositionCB }; -#if 0 -WRAPPER uint8 FindCBFunctionID(void(*f)(CAnimBlendAssociation*, void*)) { EAXJMP(0x584E70); } -#else static uint8 FindCBFunctionID(void(*f)(CAnimBlendAssociation*, void*)) { for (int i = 0; i < sizeof(CBArray) / sizeof(*CBArray); i++){ @@ -128,16 +125,12 @@ static uint8 FindCBFunctionID(void(*f)(CAnimBlendAssociation*, void*)) } return 0; } -#endif static void(*FindCBFunction(uint8 id))(CAnimBlendAssociation*, void*) { return CBArray_RE3[id]; } -#if 0 -WRAPPER static void ApplyPanelDamageToCar(uint32, CAutomobile*, bool) { EAXJMP(0x584EA0); } -#else static void ApplyPanelDamageToCar(uint32 panels, CAutomobile* vehicle, bool flying) { if(vehicle->Damage.GetPanelStatus(VEHPANEL_FRONT_LEFT) != CDamageManager::GetPanelStatus(panels, VEHPANEL_FRONT_LEFT)){ @@ -169,7 +162,6 @@ static void ApplyPanelDamageToCar(uint32 panels, CAutomobile* vehicle, bool flyi vehicle->SetPanelDamage(CAR_BUMP_REAR, VEHBUMPER_REAR, flying); } } -#endif void PrintElementsInPtrList(void) { @@ -262,9 +254,6 @@ void CReplay::Update(void) } } -#if 0 -WRAPPER void CReplay::RecordThisFrame(void) { EAXJMP(0x5932B0); } -#else void CReplay::RecordThisFrame(void) { #ifdef FIX_REPLAY_BUGS @@ -377,11 +366,7 @@ void CReplay::RecordThisFrame(void) MarkEverythingAsNew(); #endif } -#endif -#if 0 -WRAPPER void CReplay::StorePedUpdate(CPed *ped, int id) { EAXJMP(0x5935B0); } -#else void CReplay::StorePedUpdate(CPed *ped, int id) { tPedUpdatePacket* pp = (tPedUpdatePacket*)&Record.m_pBase[Record.m_nOffset]; @@ -399,11 +384,7 @@ void CReplay::StorePedUpdate(CPed *ped, int id) StorePedAnimation(ped, &pp->anim_state); Record.m_nOffset += sizeof(tPedUpdatePacket); } -#endif -#if 0 -WRAPPER void CReplay::StorePedAnimation(CPed *ped, CStoredAnimationState *state) { EAXJMP(0x593670); } -#else void CReplay::StorePedAnimation(CPed *ped, CStoredAnimationState *state) { CAnimBlendAssociation* second; @@ -442,11 +423,7 @@ void CReplay::StorePedAnimation(CPed *ped, CStoredAnimationState *state) state->partBlendAmount = 0; } } -#endif -#if 0 -WRAPPER void CReplay::StoreDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationState *state) { EAXJMP(0x593BB0); } -#else void CReplay::StoreDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationState *state) { for (int i = 0; i < NUM_MAIN_ANIMS_IN_REPLAY; i++){ @@ -503,10 +480,7 @@ void CReplay::StoreDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationState } } } -#endif -#if 0 -WRAPPER void CReplay::ProcessPedUpdate(CPed *ped, float interpolation, CAddressInReplayBuffer *buffer) { EAXJMP(0x594050); } -#else + void CReplay::ProcessPedUpdate(CPed *ped, float interpolation, CAddressInReplayBuffer *buffer) { tPedUpdatePacket *pp = (tPedUpdatePacket*)&buffer->m_pBase[buffer->m_nOffset]; @@ -543,11 +517,7 @@ void CReplay::ProcessPedUpdate(CPed *ped, float interpolation, CAddressInReplayB CWorld::Add(ped); buffer->m_nOffset += sizeof(tPedUpdatePacket); } -#endif -#if 0 -WRAPPER void CReplay::RetrievePedAnimation(CPed *ped, CStoredAnimationState *state) { EAXJMP(0x5942A0); } -#else void CReplay::RetrievePedAnimation(CPed *ped, CStoredAnimationState *state) { CAnimBlendAssociation* anim1 = CAnimManager::BlendAnimation( @@ -585,11 +555,7 @@ void CReplay::RetrievePedAnimation(CPed *ped, CStoredAnimationState *state) } } } -#endif -#if 0 -WRAPPER void CReplay::RetrieveDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationState *state) { EAXJMP(0x5944B0); } -#else void CReplay::RetrieveDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationState *state) { #ifdef FIX_REPLAY_BUGS @@ -658,11 +624,7 @@ void CReplay::RetrieveDetailedPedAnimation(CPed *ped, CStoredDetailedAnimationSt anim->SetDeleteCallback(FindCBFunction(callback & 0x7F), ped); } } -#endif -#if 0 -WRAPPER void CReplay::PlaybackThisFrame(void) { EAXJMP(0x5946B0); } -#else void CReplay::PlaybackThisFrame(void) { static int FrameSloMo = 0; @@ -687,7 +649,6 @@ void CReplay::PlaybackThisFrame(void) DMAudio.SetEffectsFadeVol(0); DMAudio.SetMusicFadeVol(0); } -#endif // next two functions are only found in mobile version // most likely they were optimized out for being unused @@ -712,9 +673,6 @@ bool CReplay::FastForwardToTime(uint32 start) return true; } -#if 0 -WRAPPER void CReplay::StoreCarUpdate(CVehicle *vehicle, int id) { EAXJMP(0x5947F0); } -#else void CReplay::StoreCarUpdate(CVehicle *vehicle, int id) { tVehicleUpdatePacket* vp = (tVehicleUpdatePacket*)&Record.m_pBase[Record.m_nOffset]; @@ -750,11 +708,7 @@ void CReplay::StoreCarUpdate(CVehicle *vehicle, int id) } Record.m_nOffset += sizeof(tVehicleUpdatePacket); } -#endif -#if 0 -WRAPPER void CReplay::ProcessCarUpdate(CVehicle *vehicle, float interpolation, CAddressInReplayBuffer *buffer) { EAXJMP(0x594D10); } -#else void CReplay::ProcessCarUpdate(CVehicle *vehicle, float interpolation, CAddressInReplayBuffer *buffer) { tVehicleUpdatePacket* vp = (tVehicleUpdatePacket*)&buffer->m_pBase[buffer->m_nOffset]; @@ -824,11 +778,7 @@ void CReplay::ProcessCarUpdate(CVehicle *vehicle, float interpolation, CAddressI ((CBoat*)vehicle)->m_bIsAnchored = false; } } -#endif -#if 0 -WRAPPER bool CReplay::PlayBackThisFrameInterpolation(CAddressInReplayBuffer *buffer, float interpolation, uint32 *pTimer) { EAXJMP(0x595240); } -#else bool CReplay::PlayBackThisFrameInterpolation(CAddressInReplayBuffer *buffer, float interpolation, uint32 *pTimer){ /* Mistake. Not even sure what this is even doing here... * PlayerWanted is a backup to restore at the end of replay. @@ -1028,10 +978,7 @@ bool CReplay::PlayBackThisFrameInterpolation(CAddressInReplayBuffer *buffer, flo ProcessReplayCamera(); return false; } -#endif -#if 0 -WRAPPER void CReplay::FinishPlayback(void) { EAXJMP(0x595B20); } -#else + void CReplay::FinishPlayback(void) { if (Mode != MODE_PLAYBACK) @@ -1053,11 +1000,7 @@ void CReplay::FinishPlayback(void) DMAudio.SetEffectsFadeVol(127); DMAudio.SetMusicFadeVol(127); } -#endif -#if 0 -WRAPPER void CReplay::EmptyReplayBuffer(void) { EAXJMP(0x595BD0); } -#else void CReplay::EmptyReplayBuffer(void) { if (Mode == MODE_PLAYBACK) @@ -1072,11 +1015,7 @@ void CReplay::EmptyReplayBuffer(void) Record.m_pBase[Record.m_nOffset] = 0; MarkEverythingAsNew(); } -#endif -#if 0 -WRAPPER void CReplay::ProcessReplayCamera(void) { EAXJMP(0x595C40); } -#else void CReplay::ProcessReplayCamera(void) { switch (CameraMode) { @@ -1120,11 +1059,7 @@ void CReplay::ProcessReplayCamera(void) RwMatrixUpdate(RwFrameGetMatrix(RwCameraGetFrame(TheCamera.m_pRwCamera))); RwFrameUpdateObjects(RwCameraGetFrame(TheCamera.m_pRwCamera)); } -#endif -#if 0 -WRAPPER void CReplay::TriggerPlayback(uint8 cam_mode, float cam_x, float cam_y, float cam_z, bool load_scene) { EAXJMP(0x596030); } -#else void CReplay::TriggerPlayback(uint8 cam_mode, float cam_x, float cam_y, float cam_z, bool load_scene) { if (Mode != MODE_RECORD) @@ -1174,11 +1109,7 @@ void CReplay::TriggerPlayback(uint8 cam_mode, float cam_x, float cam_y, float ca if (cam_mode == REPLAYCAMMODE_ASSTORED) TheCamera.CarZoomIndicator = 5.0f; } -#endif -#if 0 -WRAPPER void CReplay::StoreStuffInMem(void) { EAXJMP(0x5961F0); } -#else void CReplay::StoreStuffInMem(void) { CPools::GetVehiclePool()->Store(pBuf0, pBuf1); @@ -1223,11 +1154,7 @@ void CReplay::StoreStuffInMem(void) StoreDetailedPedAnimation(ped, &pPedAnims[i]); } } -#endif -#if 0 -WRAPPER void CReplay::RestoreStuffFromMem(void) { EAXJMP(0x5966E0); } -#else void CReplay::RestoreStuffFromMem(void) { CPools::GetVehiclePool()->CopyBack(pBuf0, pBuf1); @@ -1388,11 +1315,7 @@ void CReplay::RestoreStuffFromMem(void) DMAudio.SetRadioInCar(OldRadioStation); DMAudio.ChangeMusicMode(MUSICMODE_GAME); } -#endif -#if 0 -WRAPPER void CReplay::EmptyPedsAndVehiclePools(void) { EAXJMP(0x5970E0); } -#else void CReplay::EmptyPedsAndVehiclePools(void) { int i = CPools::GetVehiclePool()->GetSize(); @@ -1412,11 +1335,7 @@ void CReplay::EmptyPedsAndVehiclePools(void) delete p; } } -#endif -#if 0 -WRAPPER void CReplay::EmptyAllPools(void) { EAXJMP(0x5971B0); } -#else void CReplay::EmptyAllPools(void) { EmptyPedsAndVehiclePools(); @@ -1437,11 +1356,7 @@ void CReplay::EmptyAllPools(void) delete d; } } -#endif -#if 0 -WRAPPER void CReplay::MarkEverythingAsNew(void) { EAXJMP(0x597280); } -#else void CReplay::MarkEverythingAsNew(void) { int i = CPools::GetVehiclePool()->GetSize(); @@ -1459,11 +1374,7 @@ void CReplay::MarkEverythingAsNew(void) p->bHasAlreadyBeenRecorded = false; } } -#endif -#if 0 -WRAPPER void CReplay::SaveReplayToHD(void) { EAXJMP(0x597330); } -#else void CReplay::SaveReplayToHD(void) { CFileMgr::SetDirMyDocuments(); @@ -1494,11 +1405,7 @@ void CReplay::SaveReplayToHD(void) CFileMgr::CloseFile(fw); CFileMgr::SetDir(""); } -#endif -#if 0 -WRAPPER void PlayReplayFromHD(void) { EAXJMP(0x597420); } -#else void PlayReplayFromHD(void) { CFileMgr::SetDirMyDocuments(); @@ -1530,11 +1437,7 @@ void PlayReplayFromHD(void) CReplay::bAllowLookAroundCam = true; CReplay::StreamAllNecessaryCarsAndPeds(); } -#endif -#if 0 -WRAPPER void CReplay::StreamAllNecessaryCarsAndPeds(void) { EAXJMP(0x597560); } -#else void CReplay::StreamAllNecessaryCarsAndPeds(void) { for (int slot = 0; slot < NUM_REPLAYBUFFERS; slot++) { @@ -1555,11 +1458,7 @@ void CReplay::StreamAllNecessaryCarsAndPeds(void) } CStreaming::LoadAllRequestedModels(false); } -#endif -#if 0 -WRAPPER void CReplay::FindFirstFocusCoordinate(CVector *coord) { EAXJMP(0x5975E0); } -#else void CReplay::FindFirstFocusCoordinate(CVector *coord) { *coord = CVector(0.0f, 0.0f, 0.0f); @@ -1574,11 +1473,7 @@ void CReplay::FindFirstFocusCoordinate(CVector *coord) } } } -#endif -#if 0 -WRAPPER bool CReplay::ShouldStandardCameraBeProcessed(void) { EAXJMP(0x597680); } -#else bool CReplay::ShouldStandardCameraBeProcessed(void) { if (Mode != MODE_PLAYBACK) @@ -1587,11 +1482,7 @@ bool CReplay::ShouldStandardCameraBeProcessed(void) return false; return FindPlayerVehicle() != nil; } -#endif -#if 0 -WRAPPER void CReplay::ProcessLookAroundCam(void) { EAXJMP(0x5976C0); } -#else void CReplay::ProcessLookAroundCam(void) { if (!bAllowLookAroundCam) @@ -1647,11 +1538,7 @@ void CReplay::ProcessLookAroundCam(void) RwMatrixUpdate(RwFrameGetMatrix(RwCameraGetFrame(TheCamera.m_pRwCamera))); RwFrameUpdateObjects(RwCameraGetFrame(TheCamera.m_pRwCamera)); } -#endif -#if 0 -WRAPPER size_t CReplay::FindSizeOfPacket(uint8 type) { EAXJMP(0x597CC0); } -#else size_t CReplay::FindSizeOfPacket(uint8 type) { switch (type) { @@ -1669,11 +1556,7 @@ size_t CReplay::FindSizeOfPacket(uint8 type) } return 0; } -#endif -#if 0 -WRAPPER void CReplay::Display(void) { EAXJMP(0x595EE0); } -#else void CReplay::Display() { static int TimeCount = 0; @@ -1691,7 +1574,6 @@ void CReplay::Display() if (Mode == MODE_PLAYBACK) CFont::PrintString(SCREEN_SCALE_X(63.5f), SCREEN_SCALE_Y(30.0f), TheText.Get("REPLAY")); } -#endif STARTPATCHES InjectHook(0x592FE0, &CReplay::Init, PATCH_JUMP); diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 14f55734..5e83ce21 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -2842,9 +2842,6 @@ int8 CRunningScript::ProcessCommands200To299(int32 command) return -1; } -#if 0 -WRAPPER int8 CRunningScript::ProcessCommand300To399(int32 command) { EAXJMP(0x43ED30); } -#else int8 CRunningScript::ProcessCommands300To399(int32 command) { switch (command) { @@ -3576,11 +3573,7 @@ int8 CRunningScript::ProcessCommands300To399(int32 command) } return -1; } -#endif -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands400To499(int32 command) { EAXJMP(0x440CB0); } -#else int8 CRunningScript::ProcessCommands400To499(int32 command) { switch (command) { @@ -4370,11 +4363,7 @@ int8 CRunningScript::ProcessCommands400To499(int32 command) } return -1; } -#endif -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands500To599(int32 command) { EAXJMP(0x4429C0); } -#else int8 CRunningScript::ProcessCommands500To599(int32 command) { switch (command) { @@ -5201,11 +5190,7 @@ int8 CRunningScript::ProcessCommands500To599(int32 command) } return -1; } -#endif -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands600To699(int32 command) { EAXJMP(0x444B20); } -#else int8 CRunningScript::ProcessCommands600To699(int32 command) { switch (command){ @@ -5559,11 +5544,7 @@ int8 CRunningScript::ProcessCommands600To699(int32 command) } return -1; } -#endif -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands700To799(int32 command) { EAXJMP(0x4458A0); } -#else int8 CRunningScript::ProcessCommands700To799(int32 command) { switch (command){ @@ -6429,11 +6410,6 @@ int8 CRunningScript::ProcessCommands700To799(int32 command) } return -1; } -#endif - -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands800To899(int32 command) { EAXJMP(0x448240); } -#else int8 CRunningScript::ProcessCommands800To899(int32 command) { CMatrix tmp_matrix; @@ -7515,11 +7491,7 @@ int8 CRunningScript::ProcessCommands800To899(int32 command) } return -1; } -#endif -#if 0 -WRAPPER int8 CRunningScript::ProcessCommands900To999(int32 command) { EAXJMP(0x44CB80); } -#else int8 CRunningScript::ProcessCommands900To999(int32 command) { char str[52]; @@ -8420,7 +8392,6 @@ int8 CRunningScript::ProcessCommands900To999(int32 command) } return -1; } -#endif int8 CRunningScript::ProcessCommands1000To1099(int32 command) { diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 0bade6c7..7f67d609 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -1694,9 +1694,6 @@ int CMenuManager::GetStartOptionsCntrlConfigScreens() } #endif -#if DONT_USE_SUSPICIOUS_FUNCS -WRAPPER void CMenuManager::InitialiseChangedLanguageSettings() { EAXJMP(0x47A4D0); } -#else void CMenuManager::InitialiseChangedLanguageSettings() { if (m_bFrontEnd_ReloadObrTxtGxt) { @@ -1719,7 +1716,6 @@ void CMenuManager::InitialiseChangedLanguageSettings() } } } -#endif void CMenuManager::LoadAllTextures() { diff --git a/src/core/Game.cpp b/src/core/Game.cpp index fce0c67f..feabdb72 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -234,9 +234,6 @@ bool CGame::Initialise(const char* datFile) return true; } -#if 0 -WRAPPER void CGame::Process(void) { EAXJMP(0x48C850); } -#else extern void (*DebugMenuProcess)(void); void CGame::Process(void) { @@ -312,7 +309,6 @@ void CGame::Process(void) } } } -#endif void CGame::ReloadIPLs(void) { @@ -336,9 +332,6 @@ void CGame::ReloadIPLs(void) CTimer::Update(); } -#if 0 -WRAPPER void CGame::FinalShutdown(void) { EAXJMP(0x48BEC0); } -#else void CGame::FinalShutdown(void) { @@ -346,7 +339,6 @@ CGame::FinalShutdown(void) CPedStats::Shutdown(); CdStreamShutdown(); } -#endif WRAPPER bool CGame::InitialiseRenderWare(void) { EAXJMP(0x48BBA0); } WRAPPER void CGame::ShutdownRenderWare(void) { EAXJMP(0x48BCB0); } diff --git a/src/core/Radar.cpp b/src/core/Radar.cpp index 6421520b..1c634760 100644 --- a/src/core/Radar.cpp +++ b/src/core/Radar.cpp @@ -75,9 +75,6 @@ static_assert(RADAR_TILE_SIZE == (WORLD_SIZE_Y / RADAR_NUM_TILES), "CRadar: not #define RADAR_MIN_SPEED (0.3f) #define RADAR_MAX_SPEED (0.9f) -#if 0 -WRAPPER void CRadar::CalculateBlipAlpha(float) { EAXJMP(0x4A4F90); } -#else uint8 CRadar::CalculateBlipAlpha(float dist) { if (dist <= 1.0f) @@ -88,55 +85,35 @@ uint8 CRadar::CalculateBlipAlpha(float dist) return 128; } -#endif -#if 0 -WRAPPER void CRadar::ChangeBlipBrightness(int32, int32) { EAXJMP(0x4A57A0); } -#else void CRadar::ChangeBlipBrightness(int32 i, int32 bright) { int index = GetActualBlipArrayIndex(i); if (index != -1) ms_RadarTrace[index].m_bDim = bright != 1; } -#endif -#if 0 -WRAPPER void CRadar::ChangeBlipColour(int32, int32) { EAXJMP(0x4A5770); } -#else void CRadar::ChangeBlipColour(int32 i, int32 color) { int index = GetActualBlipArrayIndex(i); if (index != -1) ms_RadarTrace[index].m_nColor = color; } -#endif -#if 0 -WRAPPER void CRadar::ChangeBlipDisplay(int32, eBlipDisplay) { EAXJMP(0x4A5810); } -#else void CRadar::ChangeBlipDisplay(int32 i, eBlipDisplay display) { int index = GetActualBlipArrayIndex(i); if (index != -1) ms_RadarTrace[index].m_eBlipDisplay = display; } -#endif -#if 0 -WRAPPER void CRadar::ChangeBlipScale(int32, int32) { EAXJMP(0x4A57E0); } -#else void CRadar::ChangeBlipScale(int32 i, int32 scale) { int index = GetActualBlipArrayIndex(i); if (index != -1) ms_RadarTrace[index].m_wScale = scale; } -#endif -#if 0 -WRAPPER void CRadar::ClearBlip(int32) { EAXJMP(0x4A5720); } -#else void CRadar::ClearBlip(int32 i) { int index = GetActualBlipArrayIndex(i); @@ -148,11 +125,7 @@ void CRadar::ClearBlip(int32 i) ms_RadarTrace[index].m_IconID = RADAR_SPRITE_NONE; } } -#endif -#if 0 -WRAPPER void CRadar::ClearBlipForEntity(eBlipType, int32) { EAXJMP(0x4A56C0); } -#else void CRadar::ClearBlipForEntity(eBlipType type, int32 id) { for (int i = 0; i < NUMRADARBLIPS; i++) { @@ -165,11 +138,7 @@ void CRadar::ClearBlipForEntity(eBlipType type, int32 id) } }; } -#endif -#if 0 -WRAPPER int CRadar::ClipRadarPoly(CVector2D *poly, const CVector2D *in) { EAXJMP(0x4A64A0); } -#else // Why not a proper clipping algorithm? int CRadar::ClipRadarPoly(CVector2D *poly, const CVector2D *rect) { @@ -249,7 +218,6 @@ int CRadar::ClipRadarPoly(CVector2D *poly, const CVector2D *rect) return n; } -#endif bool CRadar::DisplayThisBlip(int32 counter) { @@ -263,9 +231,6 @@ bool CRadar::DisplayThisBlip(int32 counter) } } -#if 0 -WRAPPER void CRadar::Draw3dMarkers() { EAXJMP(0x4A4C70); } -#else void CRadar::Draw3dMarkers() { for (int i = 0; i < NUMRADARBLIPS; i++) { @@ -317,12 +282,7 @@ void CRadar::Draw3dMarkers() } } } -#endif - -#if 0 -WRAPPER void CRadar::DrawBlips() { EAXJMP(0x4A42F0); } -#else void CRadar::DrawBlips() { if (!TheCamera.m_WideScreenOn && CHud::m_Wants_To_Draw_Hud) { @@ -580,12 +540,7 @@ void CRadar::DrawBlips() } } } -#endif - -#if 0 -WRAPPER void CRadar::DrawMap () { EAXJMP(0x4A4200); } -#else void CRadar::DrawMap() { if (!TheCamera.m_WideScreenOn && CHud::m_Wants_To_Draw_Hud) { @@ -605,11 +560,7 @@ void CRadar::DrawMap() DrawRadarMap(); } } -#endif -#if 0 -WRAPPER void CRadar::DrawRadarMap() { EAXJMP(0x4A6C20); } -#else void CRadar::DrawRadarMap() { // Game calculates an unused CRect here @@ -642,11 +593,7 @@ void CRadar::DrawRadarMap() DrawRadarSection(x, y + 1); DrawRadarSection(x + 1, y + 1); } -#endif -#if 0 -WRAPPER void CRadar::DrawRadarMask() { EAXJMP(0x4A69C0); } -#else void CRadar::DrawRadarMask() { CVector2D corners[4] = { @@ -690,11 +637,7 @@ void CRadar::DrawRadarMask() RwD3D8SetRenderState(rwRENDERSTATESTENCILFUNCTION, rwSTENCILFUNCTIONGREATER); } -#endif -#if 0 -WRAPPER void CRadar::DrawRadarSection(int32, int32) { EAXJMP(0x4A67E0); } -#else void CRadar::DrawRadarSection(int32 x, int32 y) { int i; @@ -738,20 +681,12 @@ void CRadar::DrawRadarSection(int32 x, int32 y) // if(numVertices > 2) RwIm2DRenderPrimitive(rwPRIMTYPETRIFAN, CSprite2d::GetVertices(), numVertices); } -#endif -#if 0 -WRAPPER void CRadar::DrawRadarSprite(uint16 sprite, float x, float y, uint8 alpha) { EAXJMP(0x4A5EF0); } -#else void CRadar::DrawRadarSprite(uint16 sprite, float x, float y, uint8 alpha) { RadarSprites[sprite]->Draw(CRect(x - SCREEN_SCALE_X(8.0f), y - SCREEN_SCALE_Y(8.0f), x + SCREEN_SCALE_X(8.0f), y + SCREEN_SCALE_Y(8.0f)), CRGBA(255, 255, 255, alpha)); } -#endif -#if 0 -WRAPPER void CRadar::DrawRotatingRadarSprite(CSprite2d* sprite, float x, float y, float angle, int32 alpha) { EAXJMP(0x4A5D10); } -#else void CRadar::DrawRotatingRadarSprite(CSprite2d* sprite, float x, float y, float angle, int32 alpha) { CVector curPosn[4]; @@ -778,11 +713,7 @@ void CRadar::DrawRotatingRadarSprite(CSprite2d* sprite, float x, float y, float sprite->Draw(curPosn[2].x, curPosn[2].y, curPosn[3].x, curPosn[3].y, curPosn[0].x, curPosn[0].y, curPosn[1].x, curPosn[1].y, CRGBA(255, 255, 255, alpha)); } -#endif -#if 0 -WRAPPER int32 CRadar::GetActualBlipArrayIndex(int32) { EAXJMP(0x4A41C0); } -#else int32 CRadar::GetActualBlipArrayIndex(int32 i) { if (i == -1) @@ -792,11 +723,7 @@ int32 CRadar::GetActualBlipArrayIndex(int32 i) else return (uint16)i; } -#endif -#if 0 -WRAPPER int32 CRadar::GetNewUniqueBlipIndex(int32) { EAXJMP(0x4A4180); } -#else int32 CRadar::GetNewUniqueBlipIndex(int32 i) { if (ms_RadarTrace[i].m_BlipIndex >= UINT16_MAX - 1) @@ -805,11 +732,7 @@ int32 CRadar::GetNewUniqueBlipIndex(int32 i) ms_RadarTrace[i].m_BlipIndex++; return i | (ms_RadarTrace[i].m_BlipIndex << 16); } -#endif -#if 0 -WRAPPER uint32 CRadar::GetRadarTraceColour(uint32 color, bool bright) { EAXJMP(0x4A5BB0); } -#else uint32 CRadar::GetRadarTraceColour(uint32 color, bool bright) { int32 c; @@ -862,7 +785,6 @@ uint32 CRadar::GetRadarTraceColour(uint32 color, bool bright) }; return c; } -#endif const char* gRadarTexNames[] = { "radar00", "radar01", "radar02", "radar03", "radar04", "radar05", "radar06", "radar07", @@ -875,9 +797,6 @@ const char* gRadarTexNames[] = { "radar56", "radar57", "radar58", "radar59", "radar60", "radar61", "radar62", "radar63", }; -#if 0 -WRAPPER void CRadar::Initialise() { EAXJMP(0x4A3EF0); } -#else void CRadar::Initialise() { @@ -894,11 +813,7 @@ CRadar::Initialise() for (int i = 0; i < 64; i++) gRadarTxdIds[i] = CTxdStore::FindTxdSlot(gRadarTexNames[i]); } -#endif -#if 0 -WRAPPER float CRadar::LimitRadarPoint(CVector2D &point) { EAXJMP(0x4A4F30); } -#else float CRadar::LimitRadarPoint(CVector2D &point) { float dist, invdist; @@ -911,11 +826,7 @@ float CRadar::LimitRadarPoint(CVector2D &point) } return dist; } -#endif -#if 0 -WRAPPER void CRadar::LoadAllRadarBlips(int32) { EAXJMP(0x4A6F30); } -#else void CRadar::LoadAllRadarBlips(uint8 *buf, uint32 size) { Initialise(); @@ -927,11 +838,7 @@ INITSAVEBUF VALIDATESAVEBUF(size); } -#endif -#if 0 -WRAPPER void CRadar::LoadTextures() { EAXJMP(0x4A4030); } -#else void CRadar::LoadTextures() { @@ -959,42 +866,26 @@ CRadar::LoadTextures() WeaponSprite.SetTexture("radar_weapon"); CTxdStore::PopCurrentTxd(); } -#endif -#if 0 -WRAPPER void RemoveMapSection(int32, int32) { EAXJMP(0x00); } -#else void RemoveMapSection(int32 x, int32 y) { if (x >= 0 && x <= 7 && y >= 0 && y <= 7) CStreaming::RemoveTxd(gRadarTxdIds[x + RADAR_NUM_TILES * y]); } -#endif -#if 0 -WRAPPER void CRadar::RemoveRadarSections() { EAXJMP(0x4A60E0); } -#else void CRadar::RemoveRadarSections() { for (int i = 0; i < 8; i++) for (int j = 0; j < 8; j++) RemoveMapSection(i, j); } -#endif -#if 0 -WRAPPER void CRadar::RequestMapSection(int32, int32) { EAXJMP(0x00); } -#else void CRadar::RequestMapSection(int32 x, int32 y) { ClipRadarTileCoords(x, y); CStreaming::RequestTxd(gRadarTxdIds[x + RADAR_NUM_TILES * y], STREAMFLAGS_DONT_REMOVE | STREAMFLAGS_DEPENDENCY); } -#endif -#if 0 -WRAPPER void CRadar::SaveAllRadarBlips(uint8 *buf, uint32 *size) { EAXJMP(0x4A6E30); } -#else void CRadar::SaveAllRadarBlips(uint8 *buf, uint32 *size) { *size = SAVE_HEADER_SIZE + sizeof(ms_RadarTrace); @@ -1006,11 +897,7 @@ INITSAVEBUF VALIDATESAVEBUF(*size); } -#endif -#if 0 -WRAPPER void CRadar::SetBlipSprite(int32, int32) { EAXJMP(0x4A5840); } -#else void CRadar::SetBlipSprite(int32 i, int32 icon) { int index = CRadar::GetActualBlipArrayIndex(i); @@ -1018,11 +905,7 @@ void CRadar::SetBlipSprite(int32 i, int32 icon) ms_RadarTrace[index].m_IconID = icon; } } -#endif -#if 0 -WRAPPER int32 CRadar::SetCoordBlip(eBlipType, CVector, int32, eBlipDisplay display) { EAXJMP(0x4A5590); } -#else int CRadar::SetCoordBlip(eBlipType type, CVector pos, int32 color, eBlipDisplay display) { int nextBlip; @@ -1043,11 +926,7 @@ int CRadar::SetCoordBlip(eBlipType type, CVector pos, int32 color, eBlipDisplay ms_RadarTrace[nextBlip].m_IconID = RADAR_SPRITE_NONE; return CRadar::GetNewUniqueBlipIndex(nextBlip); } -#endif -#if 0 -WRAPPER int CRadar::SetEntityBlip(eBlipType type, int32, int32, eBlipDisplay) { EAXJMP(0x4A5640); } -#else int CRadar::SetEntityBlip(eBlipType type, int32 handle, int32 color, eBlipDisplay display) { int nextBlip; @@ -1066,11 +945,7 @@ int CRadar::SetEntityBlip(eBlipType type, int32 handle, int32 color, eBlipDispla ms_RadarTrace[nextBlip].m_IconID = RADAR_SPRITE_NONE; return GetNewUniqueBlipIndex(nextBlip); } -#endif -#if 0 -WRAPPER void CRadar::SetRadarMarkerState(int32, bool) { EAXJMP(0x4A5C60); } -#else void CRadar::SetRadarMarkerState(int32 counter, bool flag) { CEntity *e; @@ -1091,11 +966,7 @@ void CRadar::SetRadarMarkerState(int32 counter, bool flag) if (e) e->bHasBlip = flag; } -#endif -#if 0 -WRAPPER void CRadar::ShowRadarMarker(CVector pos, uint32 color, float radius) { EAXJMP(0x4A59C0); } -#else void CRadar::ShowRadarMarker(CVector pos, uint32 color, float radius) { float f1 = radius * 1.4f; float f2 = radius * 0.5f; @@ -1117,11 +988,7 @@ void CRadar::ShowRadarMarker(CVector pos, uint32 color, float radius) { p2 = pos - TheCamera.GetRight()*f2; CTheScripts::ScriptDebugLine3D(p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, color, color); } -#endif -#if 0 -WRAPPER void CRadar::ShowRadarTrace(float x, float y, uint32 size, uint8 red, uint8 green, uint8 blue, uint8 alpha) { EAXJMP(0x4A5870); } -#else void CRadar::ShowRadarTrace(float x, float y, uint32 size, uint8 red, uint8 green, uint8 blue, uint8 alpha) { if (!CHud::m_Wants_To_Draw_Hud || TheCamera.m_WideScreenOn) @@ -1130,7 +997,6 @@ void CRadar::ShowRadarTrace(float x, float y, uint32 size, uint8 red, uint8 gree CSprite2d::DrawRect(CRect(x - SCREEN_SCALE_X(size + 1.0f), y - SCREEN_SCALE_Y(size + 1.0f), SCREEN_SCALE_X(size + 1.0f) + x, SCREEN_SCALE_Y(size + 1.0f) + y), CRGBA(0, 0, 0, alpha)); CSprite2d::DrawRect(CRect(x - SCREEN_SCALE_X(size), y - SCREEN_SCALE_Y(size), SCREEN_SCALE_X(size) + x, SCREEN_SCALE_Y(size) + y), CRGBA(red, green, blue, alpha)); } -#endif void CRadar::ShowRadarTraceWithHeight(float x, float y, uint32 size, uint8 red, uint8 green, uint8 blue, uint8 alpha, uint8 mode) { @@ -1156,9 +1022,6 @@ void CRadar::ShowRadarTraceWithHeight(float x, float y, uint32 size, uint8 red, } } -#if 0 -WRAPPER void CRadar::Shutdown() { EAXJMP(0x4A3F60); } -#else void CRadar::Shutdown() { AsukaSprite.Delete(); @@ -1183,20 +1046,12 @@ void CRadar::Shutdown() WeaponSprite.Delete(); RemoveRadarSections(); } -#endif -#if 0 -WRAPPER void CRadar::StreamRadarSections(const CVector &posn) { EAXJMP(0x4A6B60); } -#else void CRadar::StreamRadarSections(const CVector &posn) { StreamRadarSections(floorf((2000.0f + posn.x) / 500.0f), ceilf(7.0f - (2000.0f + posn.y) / 500.0f)); } -#endif -#if 0 -WRAPPER void CRadar::StreamRadarSections(int32 x, int32 y) { EAXJMP(0x4A6100); } -#else void CRadar::StreamRadarSections(int32 x, int32 y) { for (int i = 0; i < RADAR_NUM_TILES; ++i) { @@ -1208,11 +1063,7 @@ void CRadar::StreamRadarSections(int32 x, int32 y) }; }; } -#endif -#if 0 -WRAPPER void CRadar::TransformRealWorldToTexCoordSpace(CVector2D &out, const CVector2D &in, int32 x, int32 y) { EAXJMP(0x4A5530); } -#else void CRadar::TransformRealWorldToTexCoordSpace(CVector2D &out, const CVector2D &in, int32 x, int32 y) { out.x = in.x - (x * RADAR_TILE_SIZE + WORLD_MIN_X); @@ -1220,11 +1071,7 @@ void CRadar::TransformRealWorldToTexCoordSpace(CVector2D &out, const CVector2D & out.x /= RADAR_TILE_SIZE; out.y /= RADAR_TILE_SIZE; } -#endif -#if 0 -WRAPPER void CRadar::TransformRadarPointToRealWorldSpace(CVector2D &out, const CVector2D &in) { EAXJMP(0x4A5300); } -#else void CRadar::TransformRadarPointToRealWorldSpace(CVector2D &out, const CVector2D &in) { float s, c; @@ -1255,7 +1102,6 @@ void CRadar::TransformRadarPointToRealWorldSpace(CVector2D &out, const CVector2D out = out * m_radarRange + vec2DRadarOrigin; } -#endif // Radar space goes from -1.0 to 1.0 in x and y, top right is (1.0, 1.0) void CRadar::TransformRadarPointToScreenSpace(CVector2D &out, const CVector2D &in) @@ -1265,9 +1111,6 @@ void CRadar::TransformRadarPointToScreenSpace(CVector2D &out, const CVector2D &i out.y = (1.0f - in.y)*0.5f*SCREEN_SCALE_Y(RADAR_HEIGHT) + SCREEN_SCALE_FROM_BOTTOM(RADAR_BOTTOM + RADAR_HEIGHT); } -#if 0 -WRAPPER void CRadar::TransformRealWorldPointToRadarSpace(CVector2D &out, const CVector2D &in) { EAXJMP(0x4A50D0); } -#else void CRadar::TransformRealWorldPointToRadarSpace(CVector2D &out, const CVector2D &in) { float s, c; @@ -1299,11 +1142,7 @@ void CRadar::TransformRealWorldPointToRadarSpace(CVector2D &out, const CVector2D out.x = s * y + c * x; out.y = c * y - s * x; } -#endif -#if 0 -WRAPPER void CRadar::GetTextureCorners(int32 x, int32 y, CVector2D *out) { EAXJMP(0x4A61C0); }; -#else // Transform from section indices to world coordinates void CRadar::GetTextureCorners(int32 x, int32 y, CVector2D *out) { @@ -1326,11 +1165,7 @@ void CRadar::GetTextureCorners(int32 x, int32 y, CVector2D *out) out[3].x = RADAR_TILE_SIZE * (x); out[3].y = RADAR_TILE_SIZE * (y); } -#endif -#if 0 -WRAPPER void CRadar::ClipRadarTileCoords(int32 &, int32 &) { EAXJMP(0x00); }; -#else void CRadar::ClipRadarTileCoords(int32 &x, int32 &y) { if (x < 0) @@ -1342,24 +1177,16 @@ void CRadar::ClipRadarTileCoords(int32 &x, int32 &y) if (y > RADAR_NUM_TILES-1) y = RADAR_NUM_TILES-1; } -#endif -#if 0 -WRAPPER bool CRadar::IsPointInsideRadar(const CVector2D &) { EAXJMP(0x4A6160); } -#else bool CRadar::IsPointInsideRadar(const CVector2D &point) { if (point.x < -1.0f || point.x > 1.0f) return false; if (point.y < -1.0f || point.y > 1.0f) return false; return true; } -#endif // clip line p1,p2 against (-1.0, 1.0) in x and y, set out to clipped point closest to p1 -#if 0 -WRAPPER int CRadar::LineRadarBoxCollision(CVector2D &, const CVector2D &, const CVector2D &) { EAXJMP(0x4A6250); } -#else int CRadar::LineRadarBoxCollision(CVector2D &out, const CVector2D &p1, const CVector2D &p2) { float d1, d2; @@ -1430,7 +1257,6 @@ int CRadar::LineRadarBoxCollision(CVector2D &out, const CVector2D &p1, const CVe return edge; } -#endif STARTPATCHES InjectHook(0x4A3EF0, CRadar::Initialise, PATCH_JUMP); diff --git a/src/core/Timer.cpp b/src/core/Timer.cpp index bcf84560..a46e1d8b 100644 --- a/src/core/Timer.cpp +++ b/src/core/Timer.cpp @@ -75,9 +75,6 @@ void CTimer::Shutdown(void) ; } -#if 0 -WRAPPER void CTimer::Update(void) { EAXJMP(0x4ACF70); } -#else void CTimer::Update(void) { m_snPreviousTimeInMilliseconds = m_snTimeInMilliseconds; @@ -149,7 +146,6 @@ void CTimer::Update(void) m_FrameCounter++; } -#endif void CTimer::Suspend(void) { diff --git a/src/render/Coronas.cpp b/src/render/Coronas.cpp index b0868d0a..c934540b 100644 --- a/src/render/Coronas.cpp +++ b/src/render/Coronas.cpp @@ -59,9 +59,6 @@ int &CCoronas::bChangeBrightnessImmediately = *(int*)0x8E2C30; CRegisteredCorona *CCoronas::aCoronas = (CRegisteredCorona*)0x72E518; -//WRAPPER void CCoronas::Render(void) { EAXJMP(0x4F8FB0); } -//WRAPPER void CCoronas::RenderReflections(void) { EAXJMP(0x4F9B40); } - const char aCoronaSpriteNames[][32] = { "coronastar", "corona", diff --git a/src/render/Hud.cpp b/src/render/Hud.cpp index 51aa390f..2f523e17 100644 --- a/src/render/Hud.cpp +++ b/src/render/Hud.cpp @@ -103,9 +103,6 @@ struct RwTexture *&gpSniperSightTex = *(RwTexture**)0x8F5834; RwTexture *&gpRocketSightTex = *(RwTexture**)0x8E2C20; -#if 0 -WRAPPER void CHud::Draw(void) { EAXJMP(0x5052A0); } -#else void CHud::Draw() { // disable hud via second controller @@ -1004,12 +1001,7 @@ void CHud::Draw() } } } -#endif - -#if 0 -WRAPPER void CHud::DrawAfterFade(void) { EAXJMP(0x509030); } -#else void CHud::DrawAfterFade() { if (CTimer::GetIsUserPaused() || CReplay::IsPlayingBack()) @@ -1261,11 +1253,7 @@ void CHud::DrawAfterFade() BigMessageInUse[1] = 0.0f; } } -#endif -#if 0 -WRAPPER void CHud::GetRidOfAllHudMessages(void) { EAXJMP(0x504F90); } -#else void CHud::GetRidOfAllHudMessages() { m_ZoneState = 0; @@ -1301,7 +1289,6 @@ void CHud::GetRidOfAllHudMessages() m_BigMessage[i][j] = 0; } } -#endif void CHud::Initialise() { @@ -1343,9 +1330,6 @@ void CHud::Initialise() CTxdStore::PopCurrentTxd(); } -#if 0 -WRAPPER void CHud::ReInitialise(void) { EAXJMP(0x504CC0); } -#else void CHud::ReInitialise() { m_Wants_To_Draw_Hud = true; m_Wants_To_Draw_3dMarkers = true; @@ -1367,12 +1351,9 @@ void CHud::ReInitialise() { PagerSoundPlayed = 0; PagerXOffset = 150.0f; } -#endif wchar LastBigMessage[6][128]; -#if 0 -WRAPPER void CHud::SetBigMessage(wchar *message, int16 style) { EAXJMP(0x50A250); } -#else + void CHud::SetBigMessage(wchar *message, int16 style) { int i = 0; @@ -1400,11 +1381,7 @@ void CHud::SetBigMessage(wchar *message, int16 style) LastBigMessage[style][i] = 0; m_BigMessage[style][i] = 0; } -#endif -#if 0 -WRAPPER void CHud::SetHelpMessage(wchar *message, bool quick) { EAXJMP(0x5051E0); } -#else void CHud::SetHelpMessage(wchar *message, bool quick) { if (!CReplay::IsPlayingBack()) { @@ -1419,11 +1396,7 @@ void CHud::SetHelpMessage(wchar *message, bool quick) m_HelpMessageQuick = quick; } } -#endif -#if 0 -WRAPPER void CHud::SetMessage(wchar *message) { EAXJMP(0x50A210); } -#else void CHud::SetMessage(wchar *message) { int i = 0; @@ -1435,11 +1408,7 @@ void CHud::SetMessage(wchar *message) } m_Message[i] = 0; } -#endif -#if 0 -WRAPPER void CHud::SetPagerMessage(wchar *message) { EAXJMP(0x50A320); } -#else void CHud::SetPagerMessage(wchar *message) { int i = 0; @@ -1451,25 +1420,16 @@ void CHud::SetPagerMessage(wchar *message) } m_PagerMessage[i] = 0; } -#endif -#if 0 -WRAPPER void CHud::SetVehicleName(wchar *name) { EAXJMP(0x505290); } -#else void CHud::SetVehicleName(wchar *name) { m_VehicleName = name; } -#endif -#if 0 -WRAPPER void CHud::SetZoneName(wchar *name) { EAXJMP(0x5051D0); } -#else void CHud::SetZoneName(wchar *name) { m_pZoneName = name; } -#endif void CHud::Shutdown() { diff --git a/src/save/GenericGameStorage.cpp b/src/save/GenericGameStorage.cpp index 2d5cfae2..d71b0c22 100644 --- a/src/save/GenericGameStorage.cpp +++ b/src/save/GenericGameStorage.cpp @@ -51,7 +51,6 @@ CDate &CompileDateAndTime = *(CDate*)0x72BCB8; #define ReadDataFromBufferPointer(buf, to) memcpy(&to, buf, sizeof(to)); buf += align4bytes(sizeof(to)); #define WriteDataToBufferPointer(buf, from) memcpy(buf, &from, sizeof(from)); buf += align4bytes(sizeof(from)); -//WRAPPER bool GenericSave(int file) { EAXJMP(0x58F8D0); } WRAPPER bool GenericLoad() { EAXJMP(0x590A00); } diff --git a/src/weapons/Explosion.cpp b/src/weapons/Explosion.cpp index 05087335..e99dc918 100644 --- a/src/weapons/Explosion.cpp +++ b/src/weapons/Explosion.cpp @@ -6,8 +6,6 @@ CExplosion(&gaExplosion)[48] = *(CExplosion(*)[48])*(uintptr*)0x64E208; WRAPPER void CExplosion::AddExplosion(CEntity *explodingEntity, CEntity *culprit, eExplosionType type, const CVector &pos, uint32) { EAXJMP(0x5591C0); } -//WRAPPER void CExplosion::RemoveAllExplosionsInArea(CVector, float) { EAXJMP(0x55AD40); } -//WRAPPER bool CExplosion::TestForExplosionInArea(eExplosionType, float, float, float, float, float, float) { EAXJMP(0x55AC80); } int AudioHandle = AEHANDLE_NONE; From ac097e6fc771172ace85f89a7a77d8a96242a449 Mon Sep 17 00:00:00 2001 From: aap Date: Sun, 29 Mar 2020 14:05:21 +0200 Subject: [PATCH 46/70] rotating FollowPed cam (disabled by default for now) --- src/core/Cam.cpp | 208 ++++++++++++++++++++++++++++++++++++++++++++++ src/core/Camera.h | 3 + src/core/config.h | 2 + src/core/re3.cpp | 4 + 4 files changed, 217 insertions(+) diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index 546dfde0..5844b61a 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -28,6 +28,7 @@ const float DefaultFOV = 70.0f; // beta: 80.0f bool PrintDebugCode = false; int16 &DebugCamMode = *(int16*)0x95CCF2; +bool bFreeCam = false; void CCam::Init(void) @@ -138,6 +139,11 @@ CCam::Process(void) if(CCamera::m_bUseMouse3rdPerson) Process_FollowPedWithMouse(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else +#ifdef FREE_CAM + if(bFreeCam) + Process_FollowPed_Rotation(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + else +#endif Process_FollowPed(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; // case MODE_AIMING: @@ -4369,7 +4375,209 @@ CCam::Process_FollowPed_WithBinding(const CVector &CameraTarget, float TargetOri GetVectorsReadyForRW(); } +#ifdef FREE_CAM +void +CCam::Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrientation, float, float) +{ + FOV = DefaultFOV; + + const float MinDist = 2.0f; + const float MaxDist = 2.0f + TheCamera.m_fPedZoomValueSmooth; + const float BaseOffset = 0.75f; // base height of camera above target + + CVector TargetCoors = CameraTarget; + + TargetCoors.z += m_fSyphonModeTargetZOffSet; + TargetCoors = DoAverageOnVector(TargetCoors); + TargetCoors.z += BaseOffset; // add offset so alpha evens out to 0 +// TargetCoors.z += m_fRoadOffSet; + + CVector Dist = Source - TargetCoors; + CVector ToCam; + + bool Shooting = false; + if(((CPed*)CamTargetEntity)->GetWeapon()->m_eWeaponType != WEAPONTYPE_UNARMED) + if(CPad::GetPad(0)->GetWeapon()) + Shooting = true; + if(((CPed*)CamTargetEntity)->GetWeapon()->m_eWeaponType == WEAPONTYPE_DETONATOR || + ((CPed*)CamTargetEntity)->GetWeapon()->m_eWeaponType == WEAPONTYPE_BASEBALLBAT) + Shooting = false; + + + if(ResetStatics){ + // Coming out of top down here probably + // so keep Beta, reset alpha and calculate vectors + Beta = CGeneral::GetATanOfXY(Dist.x, Dist.y); + Alpha = 0.0f; + + Dist = MaxDist*CVector(Cos(Alpha) * Cos(Beta), Cos(Alpha) * Sin(Beta), Sin(Alpha)); + Source = TargetCoors + Dist; + + ResetStatics = false; + } + + // Drag the camera along at the look-down offset + float CamDist = Dist.Magnitude(); + if(CamDist == 0.0f) + Dist = CVector(1.0f, 1.0f, 0.0f); + else if(CamDist < MinDist) + Dist *= MinDist/CamDist; + else if(CamDist > MaxDist) + Dist *= MaxDist/CamDist; + CamDist = Dist.Magnitude(); + + // Beta = 0 is looking east, HALFPI is north, &c. + // Alpha positive is looking up + float GroundDist = Dist.Magnitude2D(); + Beta = CGeneral::GetATanOfXY(-Dist.x, -Dist.y); + Alpha = CGeneral::GetATanOfXY(GroundDist, -Dist.z); + while(Beta >= PI) Beta -= 2.0f*PI; + while(Beta < -PI) Beta += 2.0f*PI; + while(Alpha >= PI) Alpha -= 2.0f*PI; + while(Alpha < -PI) Alpha += 2.0f*PI; + + // Look around + bool UseMouse = false; + float MouseX = CPad::GetPad(0)->GetMouseX(); + float MouseY = CPad::GetPad(0)->GetMouseY(); + float LookLeftRight, LookUpDown; + if((MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ + UseMouse = true; + LookLeftRight = -2.5f*MouseX; + LookUpDown = 4.0f*MouseY; + }else{ + LookLeftRight = -CPad::GetPad(0)->LookAroundLeftRight(); + LookUpDown = CPad::GetPad(0)->LookAroundUpDown(); + } + float AlphaOffset, BetaOffset; + if(UseMouse){ + BetaOffset = LookLeftRight * TheCamera.m_fMouseAccelHorzntl * FOV/80.0f; + AlphaOffset = LookUpDown * TheCamera.m_fMouseAccelVertical * FOV/80.0f; + }else{ + BetaOffset = LookLeftRight * fStickSens * (0.5f/10.0f) * FOV/80.0f * CTimer::GetTimeStep(); + AlphaOffset = LookUpDown * fStickSens * (0.3f/10.0f) * FOV/80.0f * CTimer::GetTimeStep(); + } + + // Stop centering once stick has been touched + if(BetaOffset) + Rotating = false; + + Beta += BetaOffset; + Alpha += AlphaOffset; + while(Beta >= PI) Beta -= 2.0f*PI; + while(Beta < -PI) Beta += 2.0f*PI; + if(Alpha > DEGTORAD(45.0f)) Alpha = DEGTORAD(45.0f); + if(Alpha < -DEGTORAD(89.5f)) Alpha = -DEGTORAD(89.5f); + + + float BetaDiff = TargetOrientation+PI - Beta; + while(BetaDiff >= PI) BetaDiff -= 2.0f*PI; + while(BetaDiff < -PI) BetaDiff += 2.0f*PI; + float TargetAlpha = Alpha; + // 12deg to account for our little height offset. we're not working on the true alpha here + const float AlphaLimitUp = DEGTORAD(15.0f) + DEGTORAD(12.0f); + const float AlphaLimitDown = -DEGTORAD(15.0f) + DEGTORAD(12.0f); + if(Abs(BetaDiff) < DEGTORAD(25.0f) && ((CPed*)CamTargetEntity)->GetMoveSpeed().Magnitude2D() > 0.01f){ + // Limit alpha when player is walking towards camera + if(TargetAlpha > AlphaLimitUp) TargetAlpha = AlphaLimitUp; + if(TargetAlpha < AlphaLimitDown) TargetAlpha = AlphaLimitDown; + } + + WellBufferMe(TargetAlpha, &Alpha, &AlphaSpeed, 0.2f, 0.1f, true); + + if(CPad::GetPad(0)->ForceCameraBehindPlayer() || Shooting){ + m_fTargetBeta = TargetOrientation; + Rotating = true; + } + + if(Rotating){ + WellBufferMe(m_fTargetBeta, &Beta, &BetaSpeed, 0.1f, 0.06f, true); + float DeltaBeta = m_fTargetBeta - Beta; + while(DeltaBeta >= PI) DeltaBeta -= 2*PI; + while(DeltaBeta < -PI) DeltaBeta += 2*PI; + if(Abs(DeltaBeta) < 0.06f) + Rotating = false; + } + + + Front = CVector(Cos(Alpha) * Cos(Beta), Cos(Alpha) * Sin(Beta), Sin(Alpha)); + Source = TargetCoors - Front*CamDist; + TargetCoors.z -= BaseOffset; // now get back to the real target coors again + + m_cvecTargetCoorsForFudgeInter = TargetCoors; + + + Front = TargetCoors - Source; + Front.Normalise(); + + + + /* + * Handle collisions - taken from FollowPedWithMouse + */ + + CEntity *entity; + CColPoint colPoint; + // Clip Source and fix near clip + CWorld::pIgnoreEntity = CamTargetEntity; + entity = nil; + if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, true, true, true, false, false, true)){ + float PedColDist = (TargetCoors - colPoint.point).Magnitude(); + float ColCamDist = CamDist - PedColDist; + if(entity->IsPed() && ColCamDist > 1.0f){ + // Ped in the way but not clipping through + if(CWorld::ProcessLineOfSight(colPoint.point, Source, colPoint, entity, true, true, true, true, false, false, true)){ + PedColDist = (TargetCoors - colPoint.point).Magnitude(); + Source = colPoint.point; + if(PedColDist < 0.9f + 0.3f) + RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); + }else{ + RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, 0.9f)); + } + }else{ + Source = colPoint.point; + if(PedColDist < 0.9f + 0.3f) + RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); + } + } + CWorld::pIgnoreEntity = nil; + + float ViewPlaneHeight = Tan(DEGTORAD(FOV) / 2.0f); + float ViewPlaneWidth = ViewPlaneHeight * CDraw::FindAspectRatio() * fTweakFOV; + float Near = RwCameraGetNearClipPlane(Scene.camera); + float radius = ViewPlaneWidth*Near; + entity = CWorld::TestSphereAgainstWorld(Source + Front*Near, radius, nil, true, true, false, true, false, false); + int i = 0; + while(entity){ + CVector CamToCol = gaTempSphereColPoints[0].point - Source; + float frontDist = DotProduct(CamToCol, Front); + float dist = (CamToCol - Front*frontDist).Magnitude() / ViewPlaneWidth; + + // Try to decrease near clip + dist = max(min(Near, dist), 0.1f); + if(dist < Near) + RwCameraSetNearClipPlane(Scene.camera, dist); + + // Move forward a bit + if(dist == 0.1f) + Source += (TargetCoors - Source)*0.3f; + + // Keep testing + entity = CWorld::TestSphereAgainstWorld(Source + Front*Near, radius, nil, true, true, false, true, false, false); + + i++; + if(i > 5) + entity = nil; + } + + GetVectorsReadyForRW(); +} +#endif + STARTPATCHES +#ifdef FREE_CAM + Nop(0x468E7B, 0x468E90-0x468E7B); // disable first person +#endif InjectHook(0x456F40, WellBufferMe, PATCH_JUMP); InjectHook(0x458410, &CCam::Init, PATCH_JUMP); InjectHook(0x4582F0, &CCam::GetVectorsReadyForRW, PATCH_JUMP); diff --git a/src/core/Camera.h b/src/core/Camera.h index 982620a3..3dc74fe7 100644 --- a/src/core/Camera.h +++ b/src/core/Camera.h @@ -221,6 +221,9 @@ struct CCam // CCam::Process_Blood_On_The_Tracks // CCam::Process_Cam_Running_Side_Train // CCam::Process_Cam_On_Train_Roof + + // custom stuff + void Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrientation, float, float); }; static_assert(sizeof(CCam) == 0x1A4, "CCam: wrong size"); static_assert(offsetof(CCam, Alpha) == 0xA8, "CCam: error"); diff --git a/src/core/config.h b/src/core/config.h index ba00992a..2ff0ef78 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -206,4 +206,6 @@ enum Config { // #define NEW_WALK_AROUND_ALGORITHM // to make walking around vehicles/objects less awkward #define CANCELLABLE_CAR_ENTER +// Camera #define IMPROVED_CAMERA // Better Debug cam, and maybe more in the future +//#define FREE_CAM // Rotating cam diff --git a/src/core/re3.cpp b/src/core/re3.cpp index a65e6d76..500bf230 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -372,6 +372,10 @@ DebugMenuPopulate(void) extern bool PrintDebugCode; extern int16 &DebugCamMode; +#ifdef FREE_CAM + extern bool bFreeCam; + DebugMenuAddVarBool8("Cam", "Free Cam", (int8*)&bFreeCam, nil); +#endif DebugMenuAddVarBool8("Cam", "Print Debug Code", (int8*)&PrintDebugCode, nil); DebugMenuAddVar("Cam", "Cam Mode", &DebugCamMode, nil, 1, 0, CCam::MODE_EDITOR, nil); DebugMenuAddCmd("Cam", "Normal", []() { DebugCamMode = 0; }); From d53c151ffcdfdc526069f261afd42d863f87af67 Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Sun, 29 Mar 2020 16:32:11 +0300 Subject: [PATCH 47/70] style & cosmetic fixes --- src/core/Game.cpp | 28 ++++++++++++++-------------- src/render/WeaponEffects.cpp | 36 ++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/core/Game.cpp b/src/core/Game.cpp index 57683893..8dd90d94 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -121,29 +121,29 @@ CGame::InitialiseRenderWare(void) /* Create camera */ Scene.camera = CameraCreate(RsGlobal.width, RsGlobal.height, TRUE); - ASSERT(Scene.camera != NULL); + ASSERT(Scene.camera != nil); if (!Scene.camera) { return (false); } - RwCameraSetFarClipPlane(Scene.camera, (RwReal) (2000.0)); - RwCameraSetNearClipPlane(Scene.camera, (RwReal) (0.9)); + RwCameraSetFarClipPlane(Scene.camera, 2000.0f); + RwCameraSetNearClipPlane(Scene.camera, 0.9f); - CameraSize(Scene.camera, NULL, DEFAULT_VIEWWINDOW, DEFAULT_ASPECT_RATIO); + CameraSize(Scene.camera, nil, DEFAULT_VIEWWINDOW, DEFAULT_ASPECT_RATIO); /* Create a world */ RwBBox bbox; - bbox.sup.x = bbox.sup.y = bbox.sup.z = (RwReal)(10000.0); - bbox.inf.x = bbox.inf.y = bbox.inf.z = (RwReal)(-10000.0); + bbox.sup.x = bbox.sup.y = bbox.sup.z = 10000.0f; + bbox.inf.x = bbox.inf.y = bbox.inf.z = -10000.0f; Scene.world = RpWorldCreate(&bbox); - ASSERT(Scene.world != NULL); + ASSERT(Scene.world != nil); if (!Scene.world) { CameraDestroy(Scene.camera); - Scene.camera = NULL; + Scene.camera = nil; return (false); } @@ -182,8 +182,8 @@ void CGame::ShutdownRenderWare(void) /* destroy camera */ CameraDestroy(Scene.camera); - Scene.world = NULL; - Scene.camera = NULL; + Scene.world = nil; + Scene.camera = nil; CVisibilityPlugins::Shutdown(); @@ -233,7 +233,7 @@ bool CGame::InitialiseOnceAfterRW(void) DMAudio.SetEffectsFadeVol(127); DMAudio.SetMusicFadeVol(127); CWorld::Players[0].SetPlayerSkin(CMenuManager::m_PrefsSkinFile); - + return true; } @@ -411,7 +411,7 @@ bool CGame::ShutDown(void) { CWorld::Remove(CWorld::Players[i].m_pPed); delete CWorld::Players[i].m_pPed; - CWorld::Players[i].m_pPed = NULL; + CWorld::Players[i].m_pPed = nil; } CWorld::Players[i].Clear(); @@ -490,7 +490,7 @@ void CGame::ReInitGameObjectVariables(void) CWaterCannons::Init(); CParticle::ReloadConfig(); CCullZones::ResolveVisibilities(); - + if ( !FrontEndMenuManager.m_bLoadingSavedGame ) { CCranes::InitCranes(); @@ -535,7 +535,7 @@ void CGame::ShutDownForRestart(void) for (int i = 0; i < NUMPLAYERS; i++) CWorld::Players[i].Clear(); - + CGarages::SetAllDoorsBackToOriginalHeight(); CTheScripts::UndoBuildingSwaps(); CTheScripts::UndoEntityInvisibilitySettings(); diff --git a/src/render/WeaponEffects.cpp b/src/render/WeaponEffects.cpp index 1c29caf8..cbab25a5 100644 --- a/src/render/WeaponEffects.cpp +++ b/src/render/WeaponEffects.cpp @@ -22,24 +22,24 @@ CWeaponEffects::~CWeaponEffects() void CWeaponEffects::Init(void) { - gCrossHair.m_bActive = false; - gCrossHair.m_vecPos = CVector(0.0f, 0.0f, 0.0f); - gCrossHair.m_nRed = 0; - gCrossHair.m_nGreen = 0; - gCrossHair.m_nBlue = 0; - gCrossHair.m_nAlpha = 255; - gCrossHair.m_fSize = 1.0f; - gCrossHair.m_fRotation = 0.0f; + gCrossHair.m_bActive = false; + gCrossHair.m_vecPos = CVector(0.0f, 0.0f, 0.0f); + gCrossHair.m_nRed = 0; + gCrossHair.m_nGreen = 0; + gCrossHair.m_nBlue = 0; + gCrossHair.m_nAlpha = 255; + gCrossHair.m_fSize = 1.0f; + gCrossHair.m_fRotation = 0.0f; CTxdStore::PushCurrentTxd(); - int32 slut = CTxdStore::FindTxdSlot("particle"); - CTxdStore::SetCurrentTxd(slut); + int32 slot = CTxdStore::FindTxdSlot("particle"); + CTxdStore::SetCurrentTxd(slot); gpCrossHairTex = RwTextureRead("crosshair", NULL); gpCrossHairRaster = RwTextureGetRaster(gpCrossHairTex); - CTxdStore::PopCurrentTxd(); + CTxdStore::PopCurrentTxd(); } void @@ -51,13 +51,13 @@ CWeaponEffects::Shutdown(void) void CWeaponEffects::MarkTarget(CVector pos, uint8 red, uint8 green, uint8 blue, uint8 alpha, float size) { - gCrossHair.m_bActive = true; - gCrossHair.m_vecPos = pos; - gCrossHair.m_nRed = red; - gCrossHair.m_nGreen = green; - gCrossHair.m_nBlue = blue; - gCrossHair.m_nAlpha = alpha; - gCrossHair.m_fSize = size; + gCrossHair.m_bActive = true; + gCrossHair.m_vecPos = pos; + gCrossHair.m_nRed = red; + gCrossHair.m_nGreen = green; + gCrossHair.m_nBlue = blue; + gCrossHair.m_nAlpha = alpha; + gCrossHair.m_fSize = size; } void From 7b95dcc219f220ef12df2ea63fe9671ac72268d9 Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Sun, 29 Mar 2020 16:35:50 +0300 Subject: [PATCH 48/70] style & cosmetic fixes --- src/core/Game.cpp | 8 -------- src/render/WeaponEffects.cpp | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/core/Game.cpp b/src/core/Game.cpp index 8dd90d94..e6bedf32 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -237,9 +237,6 @@ bool CGame::InitialiseOnceAfterRW(void) return true; } -#if 0 -WRAPPER void CGame::FinalShutdown(void) { EAXJMP(0x48BEC0); } -#else void CGame::FinalShutdown(void) { @@ -247,7 +244,6 @@ CGame::FinalShutdown(void) CPedStats::Shutdown(); CdStreamShutdown(); } -#endif bool CGame::Initialise(const char* datFile) { @@ -604,9 +600,6 @@ void CGame::InitialiseWhenRestarting(void) DMAudio.ChangeMusicMode(MUSICMODE_GAME); } -#if 0 -WRAPPER void CGame::Process(void) { EAXJMP(0x48C850); } -#else extern void (*DebugMenuProcess)(void); void CGame::Process(void) { @@ -682,7 +675,6 @@ void CGame::Process(void) } } } -#endif void CGame::DrasticTidyUpMemory(bool) { diff --git a/src/render/WeaponEffects.cpp b/src/render/WeaponEffects.cpp index cbab25a5..2ed9e662 100644 --- a/src/render/WeaponEffects.cpp +++ b/src/render/WeaponEffects.cpp @@ -36,7 +36,7 @@ CWeaponEffects::Init(void) int32 slot = CTxdStore::FindTxdSlot("particle"); CTxdStore::SetCurrentTxd(slot); - gpCrossHairTex = RwTextureRead("crosshair", NULL); + gpCrossHairTex = RwTextureRead("crosshair", nil); gpCrossHairRaster = RwTextureGetRaster(gpCrossHairTex); CTxdStore::PopCurrentTxd(); From 0db35f8275d59a289c4c7146b8b100fae293a632 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sun, 29 Mar 2020 18:48:57 +0300 Subject: [PATCH 49/70] more garages --- src/control/Garages.cpp | 720 +++++++++++++++++++++++++++++++++------- src/control/Garages.h | 7 +- 2 files changed, 608 insertions(+), 119 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 95f3a83d..a89f5337 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -74,6 +74,8 @@ // Collect cars stuff #define MAX_SPEED_TO_SHOW_COLLECTED_MESSAGE (0.03f) +#define IMPORT_REWARD (1000) +#define IMPORT_ALLCARS_REWARD (200000) // Crusher stuff #define CRUSHER_VEHICLE_TEST_SPAN (8) @@ -88,27 +90,34 @@ #define HIDEOUT_DOOR_SPEED_COEFFICIENT (1.7f) #define TIME_BETWEEN_HIDEOUT_MESSAGES (18000) -int32 &CGarages::BankVansCollected = *(int32 *)0x8F1B34; -bool &CGarages::BombsAreFree = *(bool *)0x95CD7A; -bool &CGarages::RespraysAreFree = *(bool *)0x95CD1D; -int32 &CGarages::CarsCollected = *(int32 *)0x880E18; -int32 (&CGarages::CarTypesCollected)[TOTAL_COLLECTCARS_GARAGES] = *(int32 (*)[TOTAL_COLLECTCARS_GARAGES])*(uintptr*)0x8E286C; -int32 &CGarages::CrushedCarId = *(int32 *)0x943060; -uint32 &CGarages::LastTimeHelpMessage = *(uint32 *)0x8F1B58; -int32 &CGarages::MessageNumberInString = *(int32 *)0x885BA8; -const char *CGarages::MessageIDString = (const char *)0x878358; -int32 &CGarages::MessageNumberInString2 = *(int32 *)0x8E2C14; -uint32 &CGarages::MessageStartTime = *(uint32 *)0x8F2530; -uint32 &CGarages::MessageEndTime = *(uint32 *)0x8F597C; -uint32 &CGarages::NumGarages = *(uint32 *)0x8F29F4; -bool &CGarages::PlayerInGarage = *(bool *)0x95CD83; -int32 &CGarages::PoliceCarsCollected = *(int32 *)0x941444; -uint32 &CGarages::GarageToBeTidied = *(uint32 *)0x623570; -CStoredCar(&CGarages::aCarsInSafeHouse1)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA210; -CStoredCar(&CGarages::aCarsInSafeHouse2)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA300; -CStoredCar(&CGarages::aCarsInSafeHouse3)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS])*(uintptr*)0x6FA3F0; +const int32 gaCarsToCollectInCraigsGarages[TOTAL_COLLECTCARS_GARAGES][TOTAL_COLLECTCARS_CARS] = +{ + { MI_SECURICA, MI_MOONBEAM, MI_COACH, MI_FLATBED, MI_LINERUN, MI_TRASH, MI_PATRIOT, MI_MRWHOOP, MI_BLISTA, MI_MULE, MI_YANKEE, MI_BOBCAT, MI_DODO, MI_BUS, MI_RUMPO, MI_PONY }, + { MI_SENTINEL, MI_CHEETAH, MI_BANSHEE, MI_IDAHO, MI_INFERNUS, MI_TAXI, MI_KURUMA, MI_STRETCH, MI_PEREN, MI_STINGER, MI_MANANA, MI_LANDSTAL, MI_STALLION, MI_BFINJECT, MI_CABBIE, MI_ESPERANT }, + { MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_LANDSTAL, MI_CHEETAH, MI_TAXI, MI_ESPERANT, MI_SENTINEL, MI_IDAHO } +}; + +int32& CGarages::BankVansCollected = *(int32*)0x8F1B34; +bool& CGarages::BombsAreFree = *(bool*)0x95CD7A; +bool& CGarages::RespraysAreFree = *(bool*)0x95CD1D; +int32& CGarages::CarsCollected = *(int32*)0x880E18; +int32(&CGarages::CarTypesCollected)[TOTAL_COLLECTCARS_GARAGES] = *(int32(*)[TOTAL_COLLECTCARS_GARAGES]) * (uintptr*)0x8E286C; +int32& CGarages::CrushedCarId = *(int32*)0x943060; +uint32& CGarages::LastTimeHelpMessage = *(uint32*)0x8F1B58; +int32& CGarages::MessageNumberInString = *(int32*)0x885BA8; +char(&CGarages::MessageIDString)[8] = *(char(*)[8]) * (uintptr*)0x878358; +int32& CGarages::MessageNumberInString2 = *(int32*)0x8E2C14; +uint32& CGarages::MessageStartTime = *(uint32*)0x8F2530; +uint32& CGarages::MessageEndTime = *(uint32*)0x8F597C; +uint32& CGarages::NumGarages = *(uint32*)0x8F29F4; +bool& CGarages::PlayerInGarage = *(bool*)0x95CD83; +int32& CGarages::PoliceCarsCollected = *(int32*)0x941444; +uint32& CGarages::GarageToBeTidied = *(uint32*)0x623570; +CStoredCar(&CGarages::aCarsInSafeHouse1)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS]) * (uintptr*)0x6FA210; +CStoredCar(&CGarages::aCarsInSafeHouse2)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS]) * (uintptr*)0x6FA300; +CStoredCar(&CGarages::aCarsInSafeHouse3)[NUM_GARAGE_STORED_CARS] = *(CStoredCar(*)[NUM_GARAGE_STORED_CARS]) * (uintptr*)0x6FA3F0; int32& CGarages::AudioEntity = *(int32*)0x5ECEA8; -CGarage(&CGarages::aGarages)[NUM_GARAGES] = *(CGarage(*)[NUM_GARAGES])*(uintptr*)0x72BCD0; +CGarage(&CGarages::aGarages)[NUM_GARAGES] = *(CGarage(*)[NUM_GARAGES]) * (uintptr*)0x72BCD0; bool& CGarages::bCamShouldBeOutisde = *(bool*)0x95CDB2; void CGarages::Init(void) @@ -167,7 +176,7 @@ void CGarages::Update(void) } if ((CTimer::GetFrameCounter() & 0xF) != 0xC) return; - if (++GarageToBeTidied >= 32) + if (++GarageToBeTidied >= NUM_GARAGES) GarageToBeTidied = 0; if (!aGarages[GarageToBeTidied].IsUsed()) return; @@ -313,12 +322,14 @@ void CGarage::Update() m_eGarageState = GS_CLOSING; CPad::GetPad(0)->SetDisablePlayerControls(PLAYERCONTROL_GARAGE); FindPlayerPed()->m_pWanted->m_bIgnoredByCops = true; - } else { + } + else { CGarages::TriggerMessage("GA_3", -1, 4000, -1); // No more freebies. $1000 to respray! m_eGarageState = GS_OPENEDCONTAINSCAR; DMAudio.PlayFrontEndSound(SOUND_GARAGE_NO_MONEY, 1); } - } else { + } + else { CGarages::TriggerMessage("GA_1", -1, 4000, -1); // Whoa! I don't touch nothing THAT hot! m_eGarageState = GS_OPENEDCONTAINSCAR; DMAudio.PlayFrontEndSound(SOUND_GARAGE_BAD_VEHICLE, 1); @@ -442,8 +453,8 @@ void CGarage::Update() if (IsPlayerOutsideGarage()) m_eGarageState = GS_OPENED; break; - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -606,9 +617,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -690,9 +701,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -729,7 +740,7 @@ void CGarage::Update() TheCamera.SetCameraDirectlyBehindForFollowPed_CamOnAString(); } } - } + } break; case GS_CLOSING: m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); @@ -751,7 +762,7 @@ void CGarage::Update() CalcSmallestDistToGarageDoorSquared( FindPlayerVehicle()->GetPosition().x, FindPlayerVehicle()->GetPosition().y - ) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) { + ) < SQR(DISTANCE_TO_ACTIVATE_GARAGE)) { if (DoesCraigNeedThisCar(FindPlayerVehicle()->GetModelIndex())) { if (FindPlayerVehicle()->VehicleCreatedBy == MISSION_VEHICLE) CGarages::TriggerMessage("GA_1A", -1, 5000, -1); // Come back when you're not so busy... @@ -779,12 +790,12 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; - } + } break; case GARAGE_FORCARTOCOMEOUTOF: switch (m_eGarageState) { @@ -811,9 +822,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -839,13 +850,13 @@ void CGarage::Update() case GS_CLOSING: if (m_pTarget) { m_fDoorPos = max(0.0f, m_fDoorPos - CRUSHER_CRANE_SPEED * CTimer::GetTimeStep()); - if (m_fDoorPos < TWOPI/5) { + if (m_fDoorPos < TWOPI / 5) { m_pTarget->bUsesCollision = false; m_pTarget->bAffectedByGravity = false; m_pTarget->SetMoveSpeed(0.0f, 0.0f, 0.0f); } else { - m_pTarget->SetMoveSpeed(m_pTarget->GetMoveSpeed()* Pow(0.8f, CTimer::GetTimeStep())); + m_pTarget->SetMoveSpeed(m_pTarget->GetMoveSpeed() * Pow(0.8f, CTimer::GetTimeStep())); } if (m_fDoorPos == 0.0f) { CGarages::CrushedCarId = CPools::GetVehiclePool()->GetIndex(m_pTarget); @@ -880,9 +891,9 @@ void CGarage::Update() } UpdateCrusherAngle(); break; - //case GS_FULLYCLOSED: - //case GS_CLOSEDCONTAINSCAR: - //case GS_OPENEDCONTAINSCAR: + //case GS_FULLYCLOSED: + //case GS_CLOSEDCONTAINSCAR: + //case GS_OPENEDCONTAINSCAR: default: break; @@ -946,8 +957,8 @@ void CGarage::Update() if (m_eGarageType == GARAGE_MISSION_KEEPCAR && CTimer::GetTimeInMilliseconds() > m_nTimeToStartAction) m_eGarageState = GS_OPENING; break; - //case GS_OPENEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -962,12 +973,12 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENED: - //case GS_CLOSING: - //case GS_FULLYCLOSED: - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENED: + //case GS_CLOSING: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -990,11 +1001,11 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENED: - //case GS_FULLYCLOSED: - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENED: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -1010,12 +1021,12 @@ void CGarage::Update() // Close car doors either if player is far, or if he is in vehicle and garage is full, // or if player is very very far so that we can remove whatever is blocking garage door without him noticing if ((distance > SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_IN_CAR) || - !FindPlayerVehicle() && distance > SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT) && + !FindPlayerVehicle() && distance > SQR(DISTANCE_TO_CLOSE_HIDEOUT_GARAGE_ON_FOOT) && !IsAnyCarBlockingDoor())) m_eGarageState = GS_CLOSING; else if (FindPlayerVehicle() && CountCarsWithCenterPointWithinGarage(FindPlayerVehicle()) >= - CGarages::FindMaxNumStoredCarsForGarage(m_eGarageType)) { + CGarages::FindMaxNumStoredCarsForGarage(m_eGarageType)) { m_eGarageState = GS_CLOSING; } else if (distance > SQR(DISTANCE_TO_FORCE_CLOSE_HIDEOUT_GARAGE)) { @@ -1025,15 +1036,9 @@ void CGarage::Update() break; } case GS_CLOSING: -#ifndef FIX_BUGS // TODO: check and replace with ifdef - if (!IsPlayerOutsideGarage()) - m_eGarageState = GS_OPENING; - m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); -#else m_fDoorPos = max(0.0f, m_fDoorPos - HIDEOUT_DOOR_SPEED_COEFFICIENT * (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); if (!IsPlayerOutsideGarage()) m_eGarageState = GS_OPENING; -#endif else if (m_fDoorPos == 0.0f) { DMAudio.PlayOneShot(CGarages::AudioEntity, SOUND_GARAGE_DOOR_CLOSED, 1.0f); m_eGarageState = GS_FULLYCLOSED; @@ -1084,9 +1089,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -1125,34 +1130,297 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } break; - //case GARAGE_COLLECTORSITEMS: - //case GARAGE_60SECONDS: + //case GARAGE_COLLECTORSITEMS: + //case GARAGE_60SECONDS: default: break; - } - + } + + } + +bool CGarage::IsStaticPlayerCarEntirelyInside() +{ + if (!FindPlayerVehicle()) + return false; + if (!FindPlayerVehicle()->IsCar()) + return false; + if (FindPlayerPed()->GetPedState() != PED_DRIVING) + return false; + if (FindPlayerPed()->m_objective == OBJECTIVE_LEAVE_VEHICLE) + return false; + CVehicle* pVehicle = FindPlayerVehicle(); + if (pVehicle->GetPosition().x < m_fX1) + return false; + if (pVehicle->GetPosition().x > m_fX2) + return false; + if (pVehicle->GetPosition().y < m_fY1) + return false; + if (pVehicle->GetPosition().y > m_fY2) + return false; + if (Abs(pVehicle->GetSpeed().x) > 0.01f) + return false; + if (Abs(pVehicle->GetSpeed().y) > 0.01f) + return false; + if (Abs(pVehicle->GetSpeed().z) > 0.01f) + return false; + if (pVehicle->GetSpeed().MagnitudeSqr() > SQR(0.01f)) + return false; + return IsEntityEntirelyInside3D(pVehicle, 0.0f); } -WRAPPER bool CGarage::IsStaticPlayerCarEntirelyInside() { EAXJMP(0x4251C0); } -WRAPPER bool CGarage::IsEntityEntirelyInside(CEntity*) { EAXJMP(0x425370); } -WRAPPER bool CGarage::IsEntityEntirelyInside3D(CEntity*, float) { EAXJMP(0x4254F0); } -WRAPPER bool CGarage::IsEntityEntirelyOutside(CEntity*, float) { EAXJMP(0x425740); } -WRAPPER bool CGarage::IsGarageEmpty() { EAXJMP(0x425890); } -WRAPPER bool CGarage::IsPlayerOutsideGarage() { EAXJMP(0x425910); } -WRAPPER bool CGarage::IsEntityTouching3D(CEntity*) { EAXJMP(0x425950); } -WRAPPER bool CGarage::EntityHasASphereWayOutsideGarage(CEntity*, float) { EAXJMP(0x425B30); } -WRAPPER bool CGarage::IsAnyOtherCarTouchingGarage(CVehicle* pException) { EAXJMP(0x425C90); } -WRAPPER bool CGarage::IsAnyOtherPedTouchingGarage(CPed* pException) { EAXJMP(0x425E20); } -WRAPPER bool CGarage::IsAnyCarBlockingDoor() { EAXJMP(0x425FB0); } -WRAPPER int32 CGarage::CountCarsWithCenterPointWithinGarage(CEntity* pException) { EAXJMP(0x426130); } -WRAPPER void CGarage::RemoveCarsBlockingDoorNotInside() { EAXJMP(0x4261F0); } +bool CGarage::IsEntityEntirelyInside(CEntity * pEntity) +{ + if (pEntity->GetPosition().x < m_fX1) + return false; + if (pEntity->GetPosition().x > m_fX2) + return false; + if (pEntity->GetPosition().y < m_fY1) + return false; + if (pEntity->GetPosition().y > m_fY2) + return false; + CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x - radius < m_fX1) + return false; + if (pos.x + radius > m_fX2) + return false; + if (pos.y - radius < m_fY1) + return false; + if (pos.y + radius > m_fY2) + return false; + } + return true; +} + +bool CGarage::IsEntityEntirelyInside3D(CEntity * pEntity, float fMargin) +{ + if (pEntity->GetPosition().x < m_fX1 - fMargin) + return false; + if (pEntity->GetPosition().x > m_fX2 + fMargin) + return false; + if (pEntity->GetPosition().y < m_fY1 - fMargin) + return false; + if (pEntity->GetPosition().y > m_fY2 + fMargin) + return false; + if (pEntity->GetPosition().z < m_fZ1 - fMargin) + return false; + if (pEntity->GetPosition().z > m_fZ2 + fMargin) + return false; + CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius < m_fX1 - fMargin) + return false; + if (pos.x - radius > m_fX2 + fMargin) + return false; + if (pos.y + radius < m_fY1 - fMargin) + return false; + if (pos.y - radius > m_fY2 + fMargin) + return false; + if (pos.z + radius < m_fZ1 - fMargin) + return false; + if (pos.z - radius > m_fZ2 + fMargin) + return false; + } + return true; +} + +bool CGarage::IsEntityEntirelyOutside(CEntity * pEntity, float fMargin) +{ + if (pEntity->GetPosition().x > m_fX1 - fMargin && pEntity->GetPosition().x < m_fX2 + fMargin && + pEntity->GetPosition().y > m_fY1 - fMargin && pEntity->GetPosition().y < m_fY2 + fMargin) + return false; + CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius > m_fX1 - fMargin && pos.x - radius < m_fX2 + fMargin && + pos.y + radius > m_fY1 - fMargin && pos.y - radius < m_fY2 + fMargin) + return false; + } + return true; +} + +bool CGarage::IsGarageEmpty() +{ + int16 num; + CWorld::FindObjectsIntersectingCube(CVector(m_fX1, m_fY1, m_fZ1), CVector(m_fX2, m_fY2, m_fZ2), &num, 2, nil, false, true, true, false, false); + return num == 0; +} + +bool CGarage::IsPlayerOutsideGarage() +{ + if (FindPlayerVehicle()) + return IsEntityEntirelyOutside(FindPlayerVehicle(), 0.0f); + return IsEntityEntirelyOutside(FindPlayerPed(), 0.0f); +} + +bool CGarage::IsEntityTouching3D(CEntity * pEntity) +{ + float radius = pEntity->GetBoundRadius(); + if (pEntity->GetPosition().x - radius < m_fX1) + return false; + if (pEntity->GetPosition().x + radius > m_fX2) + return false; + if (pEntity->GetPosition().y - radius < m_fY1) + return false; + if (pEntity->GetPosition().y + radius > m_fY2) + return false; + if (pEntity->GetPosition().z - radius < m_fZ1) + return false; + if (pEntity->GetPosition().z + radius > m_fZ2) + return false; + CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; + radius = pColModel->spheres[i].radius; + if (pos.x + radius > m_fX1 && pos.x - radius < m_fX2 && + pos.y + radius > m_fY1 && pos.y - radius < m_fY2 && + pos.z + radius > m_fZ1 && pos.z - radius < m_fZ2) + return false; + } + return true; +} + +bool CGarage::EntityHasASphereWayOutsideGarage(CEntity * pEntity, float fMargin) +{ + CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius + fMargin < m_fX1) + return true; + if (pos.x - radius - fMargin > m_fX2) + return true; + if (pos.y + radius + fMargin < m_fY1) + return true; + if (pos.y - radius - fMargin > m_fY2) + return true; + if (pos.z + radius + fMargin < m_fZ1) + return true; + if (pos.z - radius - fMargin > m_fZ2) + return true; + } + return false; +} + +bool CGarage::IsAnyOtherCarTouchingGarage(CVehicle * pException) +{ + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle || pVehicle == pException) + continue; + if (!IsEntityTouching3D(pVehicle)) + continue; + CColModel* pColModel = CModelInfo::GetModelInfo(pVehicle->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pVehicle->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius > m_fX1 && pos.x - radius < m_fX2 && + pos.y + radius > m_fY1 && pos.y - radius < m_fY2 && + pos.z + radius > m_fZ1 && pos.z - radius < m_fZ2) + return true; + } + } + return false; +} + +bool CGarage::IsAnyOtherPedTouchingGarage(CPed * pException) +{ + uint32 i = CPools::GetPedPool()->GetSize(); + while (i--) { + CPed* pPed = CPools::GetPedPool()->GetSlot(i); + if (!pPed || pPed == pException) + continue; + if (!IsEntityTouching3D(pPed)) + continue; + CColModel* pColModel = CModelInfo::GetModelInfo(pPed->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pPed->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius > m_fX1 && pos.x - radius < m_fX2 && + pos.y + radius > m_fY1 && pos.y - radius < m_fY2 && + pos.z + radius > m_fZ1 && pos.z - radius < m_fZ2) + return true; + } + } + return false; +} + +bool CGarage::IsAnyCarBlockingDoor() +{ + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle) + continue; + if (!IsEntityTouching3D(pVehicle)) + continue; + CColModel* pColModel = CModelInfo::GetModelInfo(pVehicle->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pVehicle->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius < m_fX1 || pos.x - radius > m_fX2 || + pos.y + radius < m_fY1 || pos.y - radius > m_fY2 || + pos.z + radius < m_fZ1 || pos.z - radius > m_fZ2) + return true; + } + } + return false; +} + +int32 CGarage::CountCarsWithCenterPointWithinGarage(CEntity * pException) +{ + int32 total = 0; + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle || pVehicle == pException) + continue; + if (pVehicle->GetPosition().x > m_fX1 && pVehicle->GetPosition().x < m_fX2 && + pVehicle->GetPosition().y > m_fY1 && pVehicle->GetPosition().y < m_fY2 && + pVehicle->GetPosition().z > m_fZ1 && pVehicle->GetPosition().z < m_fZ2) + total++; + } + return total; +} + +void CGarage::RemoveCarsBlockingDoorNotInside() +{ + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle) + continue; + if (!IsEntityTouching3D(pVehicle)) + continue; + CColModel* pColModel = CModelInfo::GetModelInfo(pVehicle->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pVehicle->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pVehicle->GetPosition().x < m_fX1 || pVehicle->GetPosition().x > m_fX2 || + pVehicle->GetPosition().y < m_fY1 || pVehicle->GetPosition().y > m_fY2 || + pVehicle->GetPosition().z < m_fZ1 || pVehicle->GetPosition().z > m_fZ2) { + if (pVehicle->bIsLocked && pVehicle->CanBeDeleted()) { + CWorld::Remove(pVehicle); + delete pVehicle; + return; // WHY? + } + } + } + } +} void CGarages::PrintMessages() { @@ -1166,8 +1434,11 @@ void CGarages::PrintMessages() CFont::SetFontStyle(FONT_BANK); CFont::SetColor(CRGBA(0, 0, 0, 255)); +#if defined(PS2) || defined (FIX_BUGS) float y_offset = SCREEN_HEIGHT / 3; // THIS is PS2 calculation - // y_offset = SCREEN_HEIGHT / 2 - SCREEN_SCALE_Y(84.0f); // This is PC and results in text being written over some HUD elements +#else + float y_offset = SCREEN_HEIGHT / 2 - SCREEN_SCALE_Y(84.0f); // This is PC and results in text being written over some HUD elements +#endif if (MessageNumberInString2 < 0) { if (MessageNumberInString < 0) { @@ -1195,9 +1466,50 @@ void CGarages::PrintMessages() } } -WRAPPER bool CGarages::IsCarSprayable(CVehicle*) { EAXJMP(0x426700); } -WRAPPER void CGarage::UpdateDoorsHeight() { EAXJMP(0x426730); } -WRAPPER void CGarage::BuildRotatedDoorMatrix(CEntity*, float) { EAXJMP(0x4267C0); } +bool CGarages::IsCarSprayable(CVehicle * pVehicle) +{ + switch (pVehicle->GetModelIndex()) { + case MI_FIRETRUCK: + case MI_AMBULAN: + case MI_POLICE: + case MI_ENFORCER: + case MI_BUS: + case MI_RHINO: + case MI_BARRACKS: + case MI_DODO: + case MI_COACH: + return false; + default: + break; + } + return true; +} + +void CGarage::UpdateDoorsHeight() +{ + RefreshDoorPointers(false); + if (m_pDoor1) { + m_pDoor1->GetPosition().z = m_fDoorPos + m_fDoor1Z; + if (m_bRotatedDoor) + BuildRotatedDoorMatrix(m_pDoor1, m_fDoorPos / m_fDoorHeight); + m_pDoor1->GetMatrix().UpdateRW(); + m_pDoor1->UpdateRwFrame(); + } + if (m_pDoor2) { + m_pDoor2->GetPosition().z = m_fDoorPos + m_fDoor2Z; + if (m_bRotatedDoor) + BuildRotatedDoorMatrix(m_pDoor2, m_fDoorPos / m_fDoorHeight); + m_pDoor2->GetMatrix().UpdateRW(); + m_pDoor2->UpdateRwFrame(); + } +} + +void CGarage::BuildRotatedDoorMatrix(CEntity * pDoor, float fPosition) +{ + float fAngle = -fPosition * HALFPI; + CVector r(-Sin(fAngle) * pDoor->GetForward().x, Sin(fAngle) * pDoor->GetForward().y, Cos(fAngle) * pDoor->GetForward().z); + pDoor->GetRight() = CrossProduct(r, pDoor->GetForward()); +} void CGarage::UpdateCrusherAngle() { @@ -1207,10 +1519,25 @@ void CGarage::UpdateCrusherAngle() m_pDoor2->UpdateRwFrame(); } -WRAPPER void CGarage::UpdateCrusherShake(float, float) { EAXJMP(0x4268E0); } +void CGarage::UpdateCrusherShake(float X, float Y) +{ + RefreshDoorPointers(false); + m_pDoor1->GetPosition().x += X; + m_pDoor1->GetPosition().y += Y; + m_pDoor1->GetMatrix().UpdateRW(); + m_pDoor1->UpdateRwFrame(); + m_pDoor1->GetPosition().x -= X; + m_pDoor1->GetPosition().y -= Y; + m_pDoor2->GetPosition().x += X; + m_pDoor2->GetPosition().y += Y; + m_pDoor2->GetMatrix().UpdateRW(); + m_pDoor2->UpdateRwFrame(); + m_pDoor2->GetPosition().x -= X; + m_pDoor2->GetPosition().y -= Y; +} // This is dumb but there is no way to avoid goto. What was there originally even? -static bool DoINeedToRefreshPointer(CEntity* pDoor, bool bIsDummy, int8 nIndex) +static bool DoINeedToRefreshPointer(CEntity * pDoor, bool bIsDummy, int8 nIndex) { bool bNeedToFindDoorEntities = false; if (pDoor) { @@ -1247,11 +1574,53 @@ void CGarage::RefreshDoorPointers(bool bCreate) FindDoorsEntities(); } -WRAPPER void CGarages::TriggerMessage(const char* text, int16, uint16 time, int16) { EAXJMP(0x426B20); } -WRAPPER void CGarages::SetTargetCarForMissonGarage(int16, CVehicle*) { EAXJMP(0x426BD0); } -WRAPPER bool CGarages::HasCarBeenDroppedOffYet(int16) { EAXJMP(0x426C20); } -WRAPPER void CGarages::DeActivateGarage(int16) { EAXJMP(0x426C40); } -WRAPPER void CGarages::ActivateGarage(int16) { EAXJMP(0x426C60); } +void CGarages::TriggerMessage(const char* text, int16 num1, uint16 time, int16 num2) +{ + if (strcmp(text, MessageIDString) == 0 && + CTimer::GetTimeInMilliseconds() >= MessageStartTime && + CTimer::GetTimeInMilliseconds() <= MessageEndTime) { + if (CTimer::GetTimeInMilliseconds() - MessageStartTime <= 500) + return; + MessageStartTime = CTimer::GetTimeInMilliseconds() - 500; + MessageEndTime = CTimer::GetTimeInMilliseconds() - 500 + time; + } + else { + strcpy(MessageIDString, text); + MessageStartTime = CTimer::GetTimeInMilliseconds(); + MessageEndTime = CTimer::GetTimeInMilliseconds() + time; + } + MessageNumberInString = num1; + MessageNumberInString2 = num2; +} + +void CGarages::SetTargetCarForMissonGarage(int16 garage, CVehicle * pVehicle) +{ + assert(garage >= 0 && garage < NUM_GARAGES); + if (pVehicle) { + aGarages[garage].m_pTarget = pVehicle; + if (aGarages[garage].m_eGarageState == GS_CLOSEDCONTAINSCAR) + aGarages[garage].m_eGarageState = GS_FULLYCLOSED; + } + else + aGarages[garage].m_pTarget = nil; +} + +bool CGarages::HasCarBeenDroppedOffYet(int16 garage) +{ + return aGarages[garage].m_eGarageState == GS_CLOSEDCONTAINSCAR; +} + +void CGarages::DeActivateGarage(int16 garage) +{ + aGarages[garage].m_bDeactivated = true; +} + +void CGarages::ActivateGarage(int16 garage) +{ + aGarages[garage].m_bDeactivated = false; + if (aGarages[garage].m_eGarageType == GARAGE_FORCARTOCOMEOUTOF && aGarages[garage].m_eGarageState == GS_FULLYCLOSED) + aGarages[garage].m_eGarageState = GS_OPENING; +} int32 CGarages::QueryCarsCollected(int16 garage) { @@ -1260,7 +1629,7 @@ int32 CGarages::QueryCarsCollected(int16 garage) bool CGarages::HasImportExportGarageCollectedThisCar(int16 garage, int8 car) { - return CarTypesCollected[GetCarsCollectedIndexForGarageType(aGarages[garage].m_eGarageType)] & (1 << car); + return CarTypesCollected[GetCarsCollectedIndexForGarageType(aGarages[garage].m_eGarageType)] & (BIT(car)); } bool CGarages::IsGarageOpen(int16 garage) @@ -1273,10 +1642,59 @@ bool CGarages::IsGarageClosed(int16 garage) return aGarages[garage].IsClosed(); } -WRAPPER bool CGarages::HasThisCarBeenCollected(int16 garage, uint8 id) { EAXJMP(0x426D50); } -WRAPPER bool CGarage::DoesCraigNeedThisCar(int32) { EAXJMP(0x426D90); } -WRAPPER bool CGarage::HasCraigCollectedThisCar(int32) { EAXJMP(0x426DF0); } -WRAPPER void CGarage::MarkThisCarAsCollectedForCraig(int32) { EAXJMP(0x426E50); } +bool CGarages::HasThisCarBeenCollected(int16 garage, uint8 id) +{ + return aGarages[garage].m_bCollectedCarsState & BIT(id); +} + +bool CGarage::DoesCraigNeedThisCar(int32 mi) +{ + if (mi == MI_CORPSE) + mi = MI_MANANA; + int ct = CGarages::GetCarsCollectedIndexForGarageType(m_eGarageType); + for (int i = 0; i < TOTAL_COLLECTCARS_CARS; i++) { + if (mi == gaCarsToCollectInCraigsGarages[ct][i]) + return (CGarages::CarTypesCollected[ct] & BIT(i)) == 0; + } + return false; +} + +bool CGarage::HasCraigCollectedThisCar(int32 mi) +{ + if (mi == MI_CORPSE) + mi = MI_MANANA; + int ct = CGarages::GetCarsCollectedIndexForGarageType(m_eGarageType); + for (int i = 0; i < TOTAL_COLLECTCARS_CARS; i++) { + if (mi == gaCarsToCollectInCraigsGarages[ct][i]) + return CGarages::CarTypesCollected[ct] & BIT(i); + } + return false; +} + +bool CGarage::MarkThisCarAsCollectedForCraig(int32 mi) +{ + if (mi == MI_CORPSE) + mi = MI_MANANA; + int ct = CGarages::GetCarsCollectedIndexForGarageType(m_eGarageType); + int index; + for (index = 0; index < TOTAL_COLLECTCARS_CARS; index++) { + if (mi == gaCarsToCollectInCraigsGarages[ct][index]) + break; + } + if (index >= TOTAL_COLLECTCARS_CARS) + return false; + CGarages::CarTypesCollected[ct] |= BIT(index); + CWorld::Players[CWorld::PlayerInFocus].m_nMoney += IMPORT_REWARD; + for (int i = 0; i < TOTAL_COLLECTCARS_CARS; i++) { + if ((CGarages::CarTypesCollected[ct] & BIT(i)) == 0) { + CGarages::TriggerMessage("GA_13", -1, 5000, -1); // Delivered like a pro. Complete the list and there'll be a bonus for you. + return false; + } + } + CWorld::Players[CWorld::PlayerInFocus].m_nMoney += IMPORT_ALLCARS_REWARD; + CGarages::TriggerMessage("GA_14", -1, 5000, -1); // All the cars. NICE! Here's a little something. + return true; +} void CGarage::OpenThisGarage() { @@ -1290,11 +1708,81 @@ void CGarage::CloseThisGarage() m_eGarageState = GS_CLOSING; } -WRAPPER float CGarage::CalcDistToGarageRectangleSquared(float, float) { EAXJMP(0x426F50); } -WRAPPER float CGarage::CalcSmallestDistToGarageDoorSquared(float, float) { EAXJMP(0x426FE0); } -WRAPPER void CGarage::FindDoorsEntities() { EAXJMP(0x427060); } +float CGarage::CalcDistToGarageRectangleSquared(float X, float Y) +{ + float distX, distY; + if (X < m_fX1) + distX = m_fX1 - X; + else if (X > m_fX2) + distX = X - m_fX2; + else + distX = 0.0f; + if (Y < m_fY1) + distY = m_fY1 - X; + else if (Y > m_fY2) + distY = Y - m_fY2; + else + distY = 0.0f; + return SQR(distX) + SQR(distY); +} + +float CGarage::CalcSmallestDistToGarageDoorSquared(float X, float Y) +{ + float dist1 = 10000000.0f; + float dist2 = 10000000.0f; + if (m_pDoor1) + dist1 = SQR(m_fDoor1X - X) + SQR(m_fDoor1Y - Y); + if (m_pDoor2) + dist2 = SQR(m_fDoor2X - X) + SQR(m_fDoor2Y - Y); + return min(dist1, dist2); +} + +void CGarage::FindDoorsEntities() +{ + m_pDoor1 = false; + m_pDoor2 = false; + int xstart = max(0, CWorld::GetSectorIndexX(m_fX1)); + int xend = min(NUMSECTORS_X - 1, CWorld::GetSectorIndexX(m_fX2)); + int ystart = max(0, CWorld::GetSectorIndexY(m_fY1)); + int yend = min(NUMSECTORS_Y - 1, CWorld::GetSectorIndexY(m_fY2)); + assert(xstart <= xend); + assert(ystart <= yend); + + CWorld::AdvanceCurrentScanCode(); + + for (int y = ystart; y <= yend; y++) { + for (int x = xstart; x <= xend; x++) { + CSector* s = CWorld::GetSector(x, y); + FindDoorsEntitiesSectorList(s->m_lists[ENTITYLIST_OBJECTS], false); + FindDoorsEntitiesSectorList(s->m_lists[ENTITYLIST_OBJECTS_OVERLAP], false); + FindDoorsEntitiesSectorList(s->m_lists[ENTITYLIST_DUMMIES], true); + FindDoorsEntitiesSectorList(s->m_lists[ENTITYLIST_DUMMIES_OVERLAP], true); + } + } + if (!m_pDoor1 || !m_pDoor2) + return; + if (m_pDoor1->GetModelIndex() == MI_CRUSHERBODY || m_pDoor1->GetModelIndex() == MI_CRUSHERLID) + return; + CVector2D vecDoor1ToGarage(m_pDoor1->GetPosition().x - GetGarageCenterX(), m_pDoor1->GetPosition().y - GetGarageCenterY()); + CVector2D vecDoor2ToGarage(m_pDoor2->GetPosition().x - GetGarageCenterX(), m_pDoor2->GetPosition().y - GetGarageCenterY()); + if (DotProduct2D(vecDoor1ToGarage, vecDoor2ToGarage) > 0.0f) { + if (vecDoor1ToGarage.MagnitudeSqr() >= vecDoor2ToGarage.MagnitudeSqr()) { + m_pDoor1 = m_pDoor2; + m_bDoor1IsDummy = m_bDoor2IsDummy; + } + m_pDoor2 = nil; + m_bDoor2IsDummy = false; + } +} + WRAPPER void CGarage::FindDoorsEntitiesSectorList(CPtrList&, bool) { EAXJMP(0x427300); } -WRAPPER bool CGarages::HasResprayHappened(int16 garage) { EAXJMP(0x4274F0); } + +bool CGarages::HasResprayHappened(int16 garage) +{ + bool result = aGarages[garage].m_bResprayHappened; + aGarages[garage].m_bResprayHappened = false; + return result; +} void CGarages::SetGarageDoorToRotate(int16 garage) { @@ -1310,7 +1798,7 @@ void CGarages::SetLeaveCameraForThisGarage(int16 garage) aGarages[garage].m_bCameraFollowsPlayer = true; } -WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity* pCar) { EAXJMP(0x427570); } +WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity * pCar) { EAXJMP(0x427570); } bool CGarages::HasCarBeenCrushed(int32 handle) { @@ -1342,9 +1830,9 @@ WRAPPER int32 CGarages::FindMaxNumStoredCarsForGarage(eGarageType) { EAXJMP(0x42 WRAPPER bool CGarages::IsPointWithinHideOutGarage(CVector&) { EAXJMP(0x428260); } WRAPPER bool CGarages::IsPointWithinAnyGarage(CVector&) { EAXJMP(0x428320); } WRAPPER void CGarages::SetAllDoorsBackToOriginalHeight() { EAXJMP(0x4283D0); } -WRAPPER void CGarages::Save(uint8* buf, uint32* size) { EAXJMP(0x4284E0); } +WRAPPER void CGarages::Save(uint8 * buf, uint32 * size) { EAXJMP(0x4284E0); } -CStoredCar::CStoredCar(const CStoredCar& other) +CStoredCar::CStoredCar(const CStoredCar & other) { m_nModelIndex = other.m_nModelIndex; m_vecPos = other.m_vecPos; @@ -1362,7 +1850,7 @@ CStoredCar::CStoredCar(const CStoredCar& other) m_nCarBombType = other.m_nCarBombType; } -WRAPPER void CGarages::Load(uint8* buf, uint32 size) { EAXJMP(0x428940); } +WRAPPER void CGarages::Load(uint8 * buf, uint32 size) { EAXJMP(0x428940); } bool CGarages::IsModelIndexADoor(uint32 id) @@ -1404,9 +1892,9 @@ CGarages::IsModelIndexADoor(uint32 id) STARTPATCHES - InjectHook(0x421C60, CGarages::Init, PATCH_JUMP); +InjectHook(0x421C60, CGarages::Init, PATCH_JUMP); #ifndef PS2 - InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); +InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); #endif - InjectHook(0x421E40, CGarages::Update, PATCH_JUMP); +InjectHook(0x421E40, CGarages::Update, PATCH_JUMP); ENDPATCHES \ No newline at end of file diff --git a/src/control/Garages.h b/src/control/Garages.h index e39a81fa..ffe24e3a 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -45,7 +45,8 @@ enum eGarageType : int8 enum { - TOTAL_COLLECTCARS_GARAGES = GARAGE_COLLECTCARS_3 - GARAGE_COLLECTCARS_1 + 1 + TOTAL_COLLECTCARS_GARAGES = GARAGE_COLLECTCARS_3 - GARAGE_COLLECTCARS_1 + 1, + TOTAL_COLLECTCARS_CARS = 16 }; class CStoredCar @@ -151,7 +152,7 @@ public: bool IsAnyCarBlockingDoor(); void CenterCarInGarage(CVehicle*); bool DoesCraigNeedThisCar(int32); - void MarkThisCarAsCollectedForCraig(int32); + bool MarkThisCarAsCollectedForCraig(int32); bool HasCraigCollectedThisCar(int32); bool IsGarageEmpty(); void UpdateCrusherShake(float, float); @@ -181,7 +182,7 @@ public: static int32 &CrushedCarId; static uint32 &LastTimeHelpMessage; static int32 &MessageNumberInString; - static const char *MessageIDString; + static char(&MessageIDString)[8]; static int32 &MessageNumberInString2; static uint32 &MessageStartTime; static uint32 &MessageEndTime; From 3a4e2275def684f5bcf5ed5b7f3f22ec1367bb4f Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sun, 29 Mar 2020 18:53:58 +0300 Subject: [PATCH 50/70] fixed PS2 build --- src/core/Game.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/Game.cpp b/src/core/Game.cpp index e6bedf32..3306277c 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -394,7 +394,9 @@ bool CGame::ShutDown(void) CPlane::Shutdown(); CTrain::Shutdown(); CSpecialFX::Shutdown(); +#ifndef PS2 CGarages::Shutdown(); +#endif CMovingThings::Shutdown(); gPhoneInfo.Shutdown(); CWeapon::ShutdownWeapons(); From a3b519ea6420ea6fb7fbd35cd25fbc329fab5086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Sun, 29 Mar 2020 18:26:23 +0300 Subject: [PATCH 51/70] CShotInfo, CWanted done, Frontend fix --- README.md | 7 -- src/core/EventList.cpp | 18 +---- src/core/Frontend.cpp | 49 ++++++++++---- src/core/Wanted.cpp | 125 ++++++++++++++++++++++++++++++---- src/core/Wanted.h | 4 +- src/core/World.cpp | 14 ++++ src/core/World.h | 1 + src/core/config.h | 1 + src/skel/win/win.cpp | 6 -- src/weapons/ShotInfo.cpp | 140 +++++++++++++++++++++++++++++++++++++++ src/weapons/ShotInfo.h | 23 +++++++ 11 files changed, 332 insertions(+), 56 deletions(-) create mode 100644 src/weapons/ShotInfo.cpp create mode 100644 src/weapons/ShotInfo.h diff --git a/README.md b/README.md index 85014cc1..87dbe468 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ cAudioManager - WIP CBoat CBrightLights CBulletInfo -CBulletTraces CCamera CCrane CCranes @@ -52,7 +51,6 @@ CExplosion CFallingGlassPane CFire CFireManager -CGame CGarage CGarages CGlass @@ -66,15 +64,10 @@ CRoadBlocks CRubbish CSceneEdit CSkidmarks -CShotInfo CSpecialFX CStats CTrafficLights -CWanted -CWaterCannon -CWaterCannons CWeapon -CWeaponEffects CWeather CWorld ``` diff --git a/src/core/EventList.cpp b/src/core/EventList.cpp index d72e32c4..d1c76f33 100644 --- a/src/core/EventList.cpp +++ b/src/core/EventList.cpp @@ -209,21 +209,9 @@ CEventList::ReportCrimeForEvent(eEventType type, int32 crimeId, bool copsDontCar case EVENT_CAR_SET_ON_FIRE: crime = CRIME_VEHICLE_BURNED; break; default: crime = CRIME_NONE; break; } - -#ifdef VC_PED_PORTS - if (crime == CRIME_HIT_PED && ((CPed*)crimeId)->IsPointerValid() && - FindPlayerPed()->m_pWanted->m_nWantedLevel == 0 && ((CPed*)crimeId)->bBeingChasedByPolice) { - - if(!((CPed*)crimeId)->DyingOrDead()) { - sprintf(gString, "$50 Good Citizen Bonus!"); - AsciiToUnicode(gString, gUString); - CMessages::AddBigMessage(gUString, 5000, 0); - CWorld::Players[CWorld::PlayerInFocus].m_nMoney += 50; - } - } else -#endif - if(crime == CRIME_NONE) - return; + + if(crime == CRIME_NONE) + return; CVector playerPedCoors = FindPlayerPed()->GetPosition(); CVector playerCoors = FindPlayerCoors(); diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 0bade6c7..57cab619 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -1354,23 +1354,39 @@ void CMenuManager::DrawFrontEndNormal() m_aFrontEndSprites[currentSprite].Draw(CRect(MENU_X_LEFT_ALIGNED(50.0f), MENU_Y(50.0f), MENU_X_RIGHT_ALIGNED(50.0f), SCREEN_SCALE_FROM_BOTTOM(95.0f)), CRGBA(255, 255, 255, m_nMenuFadeAlpha > 255 ? 255 : m_nMenuFadeAlpha)); + static float fadeAlpha = 0.0f; + static int lastState = 0; + + // reverseAlpha = PS2 fading (wait for 255->0, then change screen) if (m_nMenuFadeAlpha < 255) { - static uint32 LastFade = 0; + if (lastState == 1 && !reverseAlpha) + fadeAlpha = 0.f; if (m_nMenuFadeAlpha <= 0 && reverseAlpha) { reverseAlpha = false; ChangeScreen(pendingScreen, pendingOption, true, false); } else { - if (!reverseAlpha) - m_nMenuFadeAlpha += min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 20.0f; - else - m_nMenuFadeAlpha = max(0, m_nMenuFadeAlpha - min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 30.0f); + float timestep = CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); - LastFade = CTimer::GetTimeInMillisecondsPauseMode(); + // +20 per every 33 ms (1000.f/30.f - original frame limiter fps) + if (!reverseAlpha) + fadeAlpha += (timestep * 100.f) * 20.f / 33.f; + else + fadeAlpha = max(0.0f, fadeAlpha - (timestep * 100.f) * 30.f / 33.f); + + m_nMenuFadeAlpha = fadeAlpha; } + lastState = 0; } else { - if (reverseAlpha) - m_nMenuFadeAlpha -= 20; + if (lastState == 0) fadeAlpha = 255.f; + + if (reverseAlpha) { + float timestep = CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); + fadeAlpha -= (timestep * 100.f) * 30.f / 33.f; + + m_nMenuFadeAlpha = fadeAlpha; + } + lastState = 1; // TODO: what is this? waiting mouse? if(field_518 == 4){ @@ -1568,13 +1584,20 @@ void CMenuManager::DrawFrontEndNormal() } if (m_nMenuFadeAlpha < 255) { + + // Famous transparent menu bug +#ifdef FIX_BUGS + static float fadeAlpha = 0.0f; + if (m_nMenuFadeAlpha == 0 && fadeAlpha > 1.0f) fadeAlpha = 0.0f; + + float timestep = CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); + + // +20 per every 33 ms (1000.f/30.f - original frame limiter fps) + fadeAlpha += (timestep * 100.f) * 20.f / 33.f; + m_nMenuFadeAlpha = fadeAlpha; +#else static uint32 LastFade = 0; - // Famous transparent menu bug. 33.0f = 1000.f/30.f (original frame limiter fps) -#ifdef FIX_BUGS - m_nMenuFadeAlpha += min((CTimer::GetTimeInMillisecondsPauseMode() - LastFade) / 33.0f, 1.0f) * 20.0f; - LastFade = CTimer::GetTimeInMillisecondsPauseMode(); -#else if(CTimer::GetTimeInMillisecondsPauseMode() - LastFade > 10){ m_nMenuFadeAlpha += 20; LastFade = CTimer::GetTimeInMillisecondsPauseMode(); diff --git a/src/core/Wanted.cpp b/src/core/Wanted.cpp index 7af753e8..29294a2b 100644 --- a/src/core/Wanted.cpp +++ b/src/core/Wanted.cpp @@ -7,19 +7,16 @@ #include "ZoneCull.h" #include "Darkel.h" #include "DMAudio.h" +#include "CopPed.h" #include "Wanted.h" +#include "General.h" int32 &CWanted::MaximumWantedLevel = *(int32*)0x5F7714; // 6 int32 &CWanted::nMaximumWantedLevel = *(int32*)0x5F7718; // 6400 -WRAPPER void CWanted::Reset() { EAXJMP(0x4AD790) }; -WRAPPER void CWanted::Update() { EAXJMP(0x4AD7B0) }; - void CWanted::Initialise() { - int i; - m_nChaos = 0; m_nLastUpdateTime = 0; m_nLastWantedLevelChange = 0; @@ -34,10 +31,12 @@ CWanted::Initialise() m_bArmyRequired = false; m_fCrimeSensitivity = 1.0f; m_nWantedLevel = 0; - m_CopsBeatingSuspect = 0; - for(i = 0; i < 10; i++) + m_CopsBeatingSuspect = 0; + + for (int i = 0; i < ARRAY_SIZE(m_pCops); i++) m_pCops[i] = nil; - ClearQdCrimes(); + + ClearQdCrimes(); } bool @@ -61,7 +60,7 @@ CWanted::AreArmyRequired() int32 CWanted::NumOfHelisRequired() { - if (m_bIgnoredByCops) + if (m_bIgnoredByCops || m_bIgnoredByEveryone) return 0; switch (m_nWantedLevel) { @@ -79,9 +78,10 @@ CWanted::NumOfHelisRequired() void CWanted::SetWantedLevel(int32 level) { - ClearQdCrimes(); if (level > MaximumWantedLevel) level = MaximumWantedLevel; + + ClearQdCrimes(); switch (level) { case 0: m_nChaos = 0; @@ -360,10 +360,107 @@ CWanted::WorkOutPolicePresence(CVector posn, float radius) return numPolice; } +void +CWanted::Update(void) +{ + if (CTimer::GetTimeInMilliseconds() - m_nLastUpdateTime > 1000) { + if (m_nWantedLevel > 1) { + m_nLastUpdateTime = CTimer::GetTimeInMilliseconds(); + } else { + float radius = 18.0f; + CVector playerPos = FindPlayerCoors(); + if (WorkOutPolicePresence(playerPos, radius) == 0) { + m_nLastUpdateTime = CTimer::GetTimeInMilliseconds(); + m_nChaos = max(0, m_nChaos - 1); + UpdateWantedLevel(); + } + } + UpdateCrimesQ(); + bool orderMessedUp = false; + int currCopNum = 0; + bool foundEmptySlot = false; + for (int i = 0; i < ARRAY_SIZE(m_pCops); i++) { + if (m_pCops[i]) { + ++currCopNum; + if (foundEmptySlot) + orderMessedUp = true; + } else { + foundEmptySlot = true; + } + } + if (currCopNum != m_CurrentCops) { + printf("CopPursuit total messed up: re-setting\n"); + m_CurrentCops = currCopNum; + } + if (orderMessedUp) { + printf("CopPursuit pointer list messed up: re-sorting\n"); + bool fixed = true; + for (int i = 0; i < ARRAY_SIZE(m_pCops); i++) { + if (!m_pCops[i]) { + for (int j = i; j < ARRAY_SIZE(m_pCops); j++) { + if (m_pCops[j]) { + m_pCops[i] = m_pCops[j]; + m_pCops[j] = nil; + fixed = false; + break; + } + } + if (fixed) + break; + } + } + } + } +} + +void +CWanted::ResetPolicePursuit(void) +{ + for(int i = 0; i < ARRAY_SIZE(m_pCops); i++) { + CCopPed *cop = m_pCops[i]; + if (!cop) + continue; + + cop->m_bIsInPursuit = false; + cop->m_objective = OBJECTIVE_NONE; + cop->m_prevObjective = OBJECTIVE_NONE; + cop->m_nLastPedState = PED_NONE; + if (!cop->DyingOrDead()) { + cop->SetWanderPath(CGeneral::GetRandomNumberInRange(0.0f, 8.0f)); + } + m_pCops[i] = nil; + } + m_CurrentCops = 0; +} + +void +CWanted::Reset(void) +{ + ResetPolicePursuit(); + Initialise(); +} + +void +CWanted::UpdateCrimesQ(void) +{ + for(int i = 0; i < ARRAY_SIZE(m_aCrimes); i++) { + + CCrimeBeingQd &crime = m_aCrimes[i]; + if (crime.m_nType != CRIME_NONE) { + if (CTimer::GetTimeInMilliseconds() > crime.m_nTime + 500 && !crime.m_bReported) { + ReportCrimeNow(crime.m_nType, crime.m_vecPosn, crime.m_bPoliceDoesntCare); + crime.m_bReported = true; + } + if (CTimer::GetTimeInMilliseconds() > crime.m_nTime + 10000) + crime.m_nType = CRIME_NONE; + } + } +} + STARTPATCHES InjectHook(0x4AD6E0, &CWanted::Initialise, PATCH_JUMP); -// InjectHook(0x4AD790, &CWanted::Reset, PATCH_JUMP); -// InjectHook(0x4AD7B0, &CWanted::Update, PATCH_JUMP); + InjectHook(0x4AD790, &CWanted::Reset, PATCH_JUMP); + InjectHook(0x4AD7B0, &CWanted::Update, PATCH_JUMP); InjectHook(0x4AD900, &CWanted::UpdateWantedLevel, PATCH_JUMP); InjectHook(0x4AD9F0, &CWanted::RegisterCrime, PATCH_JUMP); InjectHook(0x4ADA10, &CWanted::RegisterCrime_Immediately, PATCH_JUMP); @@ -374,10 +471,10 @@ STARTPATCHES InjectHook(0x4ADBC0, &CWanted::AreFbiRequired, PATCH_JUMP); InjectHook(0x4ADBE0, &CWanted::AreArmyRequired, PATCH_JUMP); InjectHook(0x4ADC00, &CWanted::NumOfHelisRequired, PATCH_JUMP); -// InjectHook(0x4ADC40, &CWanted::ResetPolicePursuit, PATCH_JUMP); + InjectHook(0x4ADC40, &CWanted::ResetPolicePursuit, PATCH_JUMP); InjectHook(0x4ADD00, &CWanted::WorkOutPolicePresence, PATCH_JUMP); InjectHook(0x4ADF20, &CWanted::ClearQdCrimes, PATCH_JUMP); InjectHook(0x4ADFD0, &CWanted::AddCrimeToQ, PATCH_JUMP); -// InjectHook(0x4AE090, &CWanted::UpdateCrimesQ, PATCH_JUMP); + InjectHook(0x4AE090, &CWanted::UpdateCrimesQ, PATCH_JUMP); InjectHook(0x4AE110, &CWanted::ReportCrimeNow, PATCH_JUMP); ENDPATCHES diff --git a/src/core/Wanted.h b/src/core/Wanted.h index f6dbe8d0..9823529c 100644 --- a/src/core/Wanted.h +++ b/src/core/Wanted.h @@ -30,7 +30,7 @@ class CCrimeBeingQd public: eCrimeType m_nType; uint32 m_nId; - int32 m_nTime; + uint32 m_nTime; CVector m_vecPosn; bool m_bReported; bool m_bPoliceDoesntCare; @@ -78,6 +78,8 @@ public: void ReportCrimeNow(eCrimeType type, const CVector &coors, bool policeDoesntCare); void UpdateWantedLevel(); void Reset(); + void ResetPolicePursuit(); + void UpdateCrimesQ(); void Update(); bool IsIgnored(void) { return m_bIgnoredByCops || m_bIgnoredByEveryone; } diff --git a/src/core/World.cpp b/src/core/World.cpp index 1dda1056..7913589e 100644 --- a/src/core/World.cpp +++ b/src/core/World.cpp @@ -19,6 +19,7 @@ #include "Messages.h" #include "Replay.h" #include "Population.h" +#include "Fire.h" CColPoint *gaTempSphereColPoints = (CColPoint*)0x6E64C0; // [32] @@ -1051,6 +1052,19 @@ CWorld::ExtinguishAllCarFiresInArea(CVector point, float range) } } +void +CWorld::SetCarsOnFire(float x, float y, float z, float radius, CEntity *reason) +{ + int poolSize = CPools::GetVehiclePool()->GetSize(); + for (int poolIndex = poolSize - 1; poolIndex >= 0; poolIndex--) { + CVehicle *veh = CPools::GetVehiclePool()->GetSlot(poolIndex); + if (veh && veh->m_status != STATUS_WRECKED && !veh->m_pCarFire && !veh->bFireProof) { + if (Abs(veh->GetPosition().z - z) < 5.0f && Abs(veh->GetPosition().x - x) < radius && Abs(veh->GetPosition().y - y) < radius) + gFireManager.StartFire(veh, reason, 0.8f, true); + } + } +} + void CWorld::Process(void) { diff --git a/src/core/World.h b/src/core/World.h index 4b19e629..68149ab7 100644 --- a/src/core/World.h +++ b/src/core/World.h @@ -130,6 +130,7 @@ public: static void StopAllLawEnforcersInTheirTracks(); static void SetAllCarsCanBeDamaged(bool); static void ExtinguishAllCarFiresInArea(CVector, float); + static void SetCarsOnFire(float, float, float, float, CEntity*); static void Initialise(); static void AddParticles(); diff --git a/src/core/config.h b/src/core/config.h index ba00992a..c44a388a 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -100,6 +100,7 @@ enum Config { NUMPHONES = 50, NUMPEDGROUPS = 31, NUMMODELSPERPEDGROUP = 8, + NUMSHOTINFOS = 100, NUMROADBLOCKS = 600, diff --git a/src/skel/win/win.cpp b/src/skel/win/win.cpp index 4e5dccff..7993426a 100644 --- a/src/skel/win/win.cpp +++ b/src/skel/win/win.cpp @@ -2056,13 +2056,7 @@ _WinMain(HINSTANCE instance, { GetWindowPlacement(PSGLOBAL(window), &wp); - // Famous transparent menu bug. Also see the fix in Frontend.cpp -#ifdef FIX_BUGS - float ms = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); - if ((1000.0f / 100.0f) < ms && wp.showCmd != SW_SHOWMINIMIZED) -#else if (wp.showCmd != SW_SHOWMINIMIZED) -#endif RsEventHandler(rsFRONTENDIDLE, nil); if ( !FrontEndMenuManager.m_bMenuActive || FrontEndMenuManager.m_bLoadingSavedGame ) diff --git a/src/weapons/ShotInfo.cpp b/src/weapons/ShotInfo.cpp new file mode 100644 index 00000000..43d0579d --- /dev/null +++ b/src/weapons/ShotInfo.cpp @@ -0,0 +1,140 @@ +#include "common.h" +#include "patcher.h" +#include "ShotInfo.h" +#include "Entity.h" +#include "Weapon.h" +#include "World.h" +#include "WeaponInfo.h" +#include "General.h" +#include "Timer.h" +#include "Ped.h" +#include "Fire.h" + +CShotInfo gaShotInfo[NUMSHOTINFOS]; +float CShotInfo::ms_afRandTable[20]; + +// CShotInfo (&gaShotInfo)[100] = *(CShotInfo(*)[100])*(uintptr*)0x64F0D0; +// float (&CShotInfo::ms_afRandTable)[20] = *(float(*)[20])*(uintptr*)0x6E9878; + +/* + Used for flamethrower. I don't know why it's name is CShotInfo. + Has no relation with any visual, just calculates the area fire affects + (including spreading and slowing of fire) and make entities burn/flee. +*/ + +void +CShotInfo::Initialise() +{ + debug("Initialising CShotInfo...\n"); + for(int i=0; im_fRadius; + + if (weaponInfo->m_fSpread != 0.0f) { + gaShotInfo[slot].m_areaAffected.x += CShotInfo::ms_afRandTable[CGeneral::GetRandomNumber() % ARRAY_SIZE(ms_afRandTable)] * weaponInfo->m_fSpread; + gaShotInfo[slot].m_areaAffected.y += CShotInfo::ms_afRandTable[CGeneral::GetRandomNumber() % ARRAY_SIZE(ms_afRandTable)] * weaponInfo->m_fSpread; + gaShotInfo[slot].m_areaAffected.z += CShotInfo::ms_afRandTable[CGeneral::GetRandomNumber() % ARRAY_SIZE(ms_afRandTable)]; + } + gaShotInfo[slot].m_areaAffected.Normalise(); + if (weaponInfo->m_bRandSpeed) + gaShotInfo[slot].m_areaAffected *= CShotInfo::ms_afRandTable[CGeneral::GetRandomNumber() % ARRAY_SIZE(ms_afRandTable)] + weaponInfo->m_fSpeed; + else + gaShotInfo[slot].m_areaAffected *= weaponInfo->m_fSpeed; + + gaShotInfo[slot].m_sourceEntity = sourceEntity; + gaShotInfo[slot].m_timeout = CTimer::GetTimeInMilliseconds() + weaponInfo->m_fLifespan; + + return true; +} + +void +CShotInfo::Shutdown() +{ + debug("Shutting down CShotInfo...\n"); + debug("CShotInfo shut down\n"); +} + +void +CShotInfo::Update() +{ + for (int slot = 0; slot < ARRAY_SIZE(gaShotInfo); slot++) { + CShotInfo &shot = gaShotInfo[slot]; + if (shot.m_sourceEntity && shot.m_sourceEntity->IsPed() && !((CPed*)shot.m_sourceEntity)->IsPointerValid()) + shot.m_sourceEntity = nil; + + if (!shot.m_inUse) + continue; + + CWeaponInfo *weaponInfo = CWeaponInfo::GetWeaponInfo(shot.m_weapon); + if (CTimer::GetTimeInMilliseconds() > shot.m_timeout) + shot.m_inUse = false; + + if (weaponInfo->m_bSlowsDown) + shot.m_areaAffected *= pow(0.96, CTimer::GetTimeStep()); // FRAMERATE + + if (weaponInfo->m_bExpands) + shot.m_radius += 0.075f * CTimer::GetTimeStep(); + + shot.m_startPos += CTimer::GetTimeStep() * shot.m_areaAffected; + if (shot.m_sourceEntity) { + assert(shot.m_sourceEntity->IsPed()); + CPed *ped = (CPed*) shot.m_sourceEntity; + float radius = max(1.0f, shot.m_radius); + + for (int i = 0; i < ped->m_numNearPeds; ++i) { + CPed *nearPed = ped->m_nearPeds[i]; + if (nearPed->IsPointerValid()) { + if (nearPed->IsPedInControl() && (nearPed->GetPosition() - shot.m_startPos).MagnitudeSqr() < radius && !nearPed->bFireProof) { + + if (!nearPed->IsPlayer()) { + nearPed->SetFindPathAndFlee(shot.m_sourceEntity, 10000); + nearPed->SetMoveState(PEDMOVE_SPRINT); + } + gFireManager.StartFire(nearPed, shot.m_sourceEntity, 0.8f, true); + } + } + } + } + if (!((CTimer::GetFrameCounter() + slot) & 3)) + CWorld::SetCarsOnFire(shot.m_startPos.x, shot.m_startPos.y, shot.m_startPos.z, 4.0f, shot.m_sourceEntity); + } +} + +STARTPATCHES + InjectHook(0x55BFF0, &CShotInfo::Update, PATCH_JUMP); + InjectHook(0x55BD70, &CShotInfo::AddShot, PATCH_JUMP); + InjectHook(0x55BC60, &CShotInfo::Initialise, PATCH_JUMP); + InjectHook(0x55BD50, &CShotInfo::Shutdown, PATCH_JUMP); +ENDPATCHES \ No newline at end of file diff --git a/src/weapons/ShotInfo.h b/src/weapons/ShotInfo.h new file mode 100644 index 00000000..a5e5fd35 --- /dev/null +++ b/src/weapons/ShotInfo.h @@ -0,0 +1,23 @@ +#pragma once + +class CEntity; +enum eWeaponType; + +class CShotInfo +{ +public: + eWeaponType m_weapon; + CVector m_startPos; + CVector m_areaAffected; + float m_radius; + CEntity *m_sourceEntity; + float m_timeout; + bool m_inUse; + + static float ms_afRandTable[20]; + + static void Initialise(void); + static bool AddShot(CEntity*, eWeaponType, CVector, CVector); + static void Shutdown(void); + static void Update(void); +}; From d374d74115495e6aa1f90c9a2a924488db6f4940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Sun, 29 Mar 2020 19:14:29 +0300 Subject: [PATCH 52/70] Update Readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 87dbe468..5d5180fc 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,6 @@ CCullZone CCullZones CExplosion CFallingGlassPane -CFire -CFireManager CGarage CGarages CGlass From bb8868eba79e0c6b76ca1e5a397ac20e72937798 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Sun, 29 Mar 2020 09:35:13 +0300 Subject: [PATCH 53/70] Add russian lang support --- gamefiles/JAPANESE.gxt | Bin 0 -> 122022 bytes gamefiles/fonts_j.txd | Bin 0 -> 1052072 bytes gamefiles/fonts_r.txd | Bin 0 -> 1379752 bytes gamefiles/russian.gxt | Bin 0 -> 220394 bytes src/core/Frontend.cpp | 24 + src/core/Frontend.h | 6 + src/core/Game.cpp | 3 + src/core/Game.h | 3 + src/core/MenuScreens.h | 3 + src/core/config.h | 1 + src/render/Font.cpp | 1151 ++++++++++++++++++++++------------------ src/render/Font.h | 16 + src/text/Text.cpp | 5 + 13 files changed, 697 insertions(+), 515 deletions(-) create mode 100644 gamefiles/JAPANESE.gxt create mode 100644 gamefiles/fonts_j.txd create mode 100644 gamefiles/fonts_r.txd create mode 100644 gamefiles/russian.gxt diff --git a/gamefiles/JAPANESE.gxt b/gamefiles/JAPANESE.gxt new file mode 100644 index 0000000000000000000000000000000000000000..d4b5438388b4c17680b769fa84ce099d8587bb3b GIT binary patch literal 122022 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⪚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 z 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 z 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 z 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 X0wh2JBtQc9MPP64{P*APH6H&DWV&GK literal 0 HcmV?d00001 diff --git a/gamefiles/fonts_r.txd b/gamefiles/fonts_r.txd new file mode 100644 index 0000000000000000000000000000000000000000..4b89e4492a72ebe4cbf892ea541140c17c8f8da9 GIT binary patch literal 1379752 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�ScLK6 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

A^e==1yV6n~!HQu%#(o?acV(FAvFP^Eh0 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

5Tdh7{5BMz5+0WzJ~ujoqN3dw4C|n9%1|) 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

EBHLMqI7`-Q4FQ#{8|@ADCHVc7eNlSPAc` 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 z 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

G;T5l_g`5%S1Q1O(DDe&JQ?9PFK6UR`BK& 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 KZ?e^Ud;bqIe5?8Z literal 0 HcmV?d00001 diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 0bade6c7..a002835b 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -1707,6 +1707,17 @@ void CMenuManager::InitialiseChangedLanguageSettings() CTimer::Update(); CGame::frenchGame = false; CGame::germanGame = false; +#ifdef MORE_LANGUAGES + switch (CMenuManager::m_PrefsLanguage) { + case LANGUAGE_RUSSIAN: + CFont::ReloadFonts(FONT_LANGSET_RUSSIAN); + break; + default: + CFont::ReloadFonts(FONT_LANGSET_EFIGS); + break; + } +#endif + switch (CMenuManager::m_PrefsLanguage) { case LANGUAGE_FRENCH: CGame::frenchGame = true; @@ -1714,6 +1725,11 @@ void CMenuManager::InitialiseChangedLanguageSettings() case LANGUAGE_GERMAN: CGame::germanGame = true; break; +#ifdef MORE_LANGUAGES + case LANGUAGE_RUSSIAN: + CGame::russianGame = true; + break; +#endif default: break; } @@ -2916,6 +2932,14 @@ CMenuManager::ProcessButtonPresses(void) CMenuManager::InitialiseChangedLanguageSettings(); SaveSettings(); break; +#ifdef MORE_LANGUAGES + case MENUACTION_LANG_RUS: + m_PrefsLanguage = LANGUAGE_RUSSIAN; + m_bFrontEnd_ReloadObrTxtGxt = true; + CMenuManager::InitialiseChangedLanguageSettings(); + SaveSettings(); + break; +#endif case MENUACTION_POPULATESLOTS_CHANGEMENU: PcSaveHelper.PopulateSlotInfo(); diff --git a/src/core/Frontend.h b/src/core/Frontend.h index 3dbed164..c055c6ab 100644 --- a/src/core/Frontend.h +++ b/src/core/Frontend.h @@ -51,6 +51,9 @@ enum eLanguages LANGUAGE_GERMAN, LANGUAGE_ITALIAN, LANGUAGE_SPANISH, +#ifdef MORE_LANGUAGES + LANGUAGE_RUSSIAN, +#endif }; enum eFrontendSprites @@ -301,6 +304,9 @@ enum eMenuAction MENUACTION_UNK108, MENUACTION_UNK109, MENUACTION_UNK110, +#ifdef MORE_LANGUAGES + MENUACTION_LANG_RUS, +#endif }; enum eCheckHover diff --git a/src/core/Game.cpp b/src/core/Game.cpp index fce0c67f..3170ab6c 100644 --- a/src/core/Game.cpp +++ b/src/core/Game.cpp @@ -78,6 +78,9 @@ bool &CGame::germanGame = *(bool*)0x95CD1E; bool &CGame::noProstitutes = *(bool*)0x95CDCF; bool &CGame::playingIntro = *(bool*)0x95CDC2; char *CGame::aDatFile = (char*)0x773A48; +#ifdef MORE_LANGUAGES +bool CGame::russianGame = false; +#endif bool diff --git a/src/core/Game.h b/src/core/Game.h index 7b20099c..991c8718 100644 --- a/src/core/Game.h +++ b/src/core/Game.h @@ -16,6 +16,9 @@ public: static bool &nastyGame; static bool &frenchGame; static bool &germanGame; +#ifdef MORE_LANGUAGES + static bool russianGame; +#endif static bool &noProstitutes; static bool &playingIntro; static char *aDatFile; //[32]; diff --git a/src/core/MenuScreens.h b/src/core/MenuScreens.h index 427d01bf..ace6a719 100644 --- a/src/core/MenuScreens.h +++ b/src/core/MenuScreens.h @@ -65,6 +65,9 @@ const CMenuScreen aScreens[] = { MENUACTION_LANG_GER, "FEL_GER", SAVESLOT_NONE, MENUPAGE_NONE, MENUACTION_LANG_ITA, "FEL_ITA", SAVESLOT_NONE, MENUPAGE_NONE, MENUACTION_LANG_SPA, "FEL_SPA", SAVESLOT_NONE, MENUPAGE_NONE, +#ifdef MORE_LANGUAGES + MENUACTION_LANG_RUS, "FEL_RUS", SAVESLOT_NONE, MENUPAGE_NONE, +#endif MENUACTION_CHANGEMENU, "FEDS_TB", SAVESLOT_NONE, MENUPAGE_NONE, }, diff --git a/src/core/config.h b/src/core/config.h index ba00992a..4015e868 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -172,6 +172,7 @@ enum Config { #define FIX_BUGS // fixes bugs that we've came across during reversing, TODO: use this more #define TOGGLEABLE_BETA_FEATURES // toggleable from debug menu. not too many things +#define MORE_LANGUAGES // Add more translations to the game // Pad #define XINPUT diff --git a/src/render/Font.cpp b/src/render/Font.cpp index d7b4b5d8..0d79eee3 100644 --- a/src/render/Font.cpp +++ b/src/render/Font.cpp @@ -1,515 +1,636 @@ -#include "common.h" -#include "patcher.h" -#include "Sprite2d.h" -#include "TxdStore.h" -#include "Font.h" - -CFontDetails &CFont::Details = *(CFontDetails*)0x8F317C; -int16 &CFont::NewLine = *(int16*)0x95CC94; -CSprite2d *CFont::Sprite = (CSprite2d*)0x95CC04; - -int16 CFont::Size[3][193] = { - { - 13, 12, 31, 35, 23, 35, 31, 9, 14, 15, 25, 30, 11, 17, 13, 31, - 23, 16, 22, 21, 24, 23, 23, 20, 23, 22, 10, 35, 26, 26, 26, 26, - 30, 26, 24, 23, 24, 22, 21, 24, 26, 10, 20, 26, 22, 29, 26, 25, - 23, 25, 24, 24, 22, 25, 24, 29, 29, 23, 25, 37, 22, 37, 35, 37, - 35, 21, 22, 21, 21, 22, 13, 22, 21, 10, 16, 22, 11, 32, 21, 21, - 23, 22, 16, 20, 14, 21, 20, 30, 25, 21, 21, 33, 33, 33, 33, 35, - 27, 27, 27, 27, 32, 24, 23, 23, 23, 23, 11, 11, 11, 11, 26, 26, - 26, 26, 26, 26, 26, 25, 26, 21, 21, 21, 21, 32, 23, 22, 22, 22, - 22, 11, 11, 11, 11, 22, 22, 22, 22, 22, 22, 22, 22, 26, 21, 24, - 12, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 18, 26, 26, - 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, - 20 - }, - - { - 13, 9, 21, 35, 23, 35, 35, 11, 35, 35, 25, 35, 11, 17, 13, 33, - 28, 14, 22, 21, 24, 23, 23, 21, 23, 22, 10, 35, 13, 35, 13, 33, - 5, 25, 22, 23, 24, 21, 21, 24, 24, 9, 20, 24, 21, 27, 25, 25, - 22, 25, 23, 20, 23, 23, 23, 31, 23, 23, 23, 37, 33, 37, 35, 37, - 35, 21, 19, 19, 21, 19, 17, 21, 21, 8, 17, 18, 14, 24, 21, 21, - 20, 22, 19, 20, 20, 19, 20, 26, 21, 20, 21, 33, 33, 33, 33, 35, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 16 - }, - - { - 15, 14, 16, 25, 19, 26, 22, 11, 18, 18, 27, 26, 13, 19, 9, 27, - 19, 18, 19, 19, 22, 19, 20, 18, 19, 20, 12, 32, 15, 32, 15, 35, - 15, 19, 19, 19, 19, 19, 16, 19, 20, 9, 19, 20, 14, 29, 19, 20, - 19, 19, 19, 19, 21, 19, 20, 32, 20, 19, 19, 33, 31, 39, 37, 39, - 37, 21, 21, 21, 23, 21, 19, 23, 23, 10, 19, 20, 16, 26, 23, 23, - 20, 20, 20, 22, 21, 22, 22, 26, 22, 22, 23, 35, 35, 35, 35, 37, - 19, 19, 19, 19, 29, 19, 19, 19, 19, 19, 9, 9, 9, 9, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 30, 19, 19, 19, 19, - 19, 10, 10, 10, 10, 19, 19, 19, 19, 19, 19, 19, 19, 19, 23, 35, - 12, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 11, 19, 19, - 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, - 19 - } -}; - -uint16 foreign_table[128] = { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 176, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 177, 0, 0, 0, 0, 0, 95, 0, 0, 0, 0, 175, - 128, 129, 130, 0, 131, 0, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, - 0, 173, 142, 143, 144, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 150, - 151, 152, 153, 0, 154, 0, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, - 0, 174, 165, 166, 167, 0, 168, 0, 0, 169, 170, 171, 172, 0, 0, 0, -}; - -void -CFont::Initialise(void) -{ - int slot; - - slot = CTxdStore::AddTxdSlot("fonts"); - CTxdStore::LoadTxd(slot, "MODELS/FONTS.TXD"); - CTxdStore::AddRef(slot); - CTxdStore::PushCurrentTxd(); - CTxdStore::SetCurrentTxd(slot); - Sprite[0].SetTexture("font2", "font2_mask"); - Sprite[1].SetTexture("pager", "pager_mask"); - Sprite[2].SetTexture("font1", "font1_mask"); - SetScale(1.0f, 1.0f); - SetSlantRefPoint(SCREEN_WIDTH, 0.0f); - SetSlant(0.0f); - SetColor(CRGBA(0xFF, 0xFF, 0xFF, 0)); - SetJustifyOff(); - SetCentreOff(); - SetWrapx(640.0f); - SetCentreSize(640.0f); - SetBackgroundOff(); - SetBackgroundColor(CRGBA(0x80, 0x80, 0x80, 0x80)); - SetBackGroundOnlyTextOff(); - SetPropOn(); - SetFontStyle(FONT_BANK); - SetRightJustifyWrap(0.0f); - SetAlphaFade(255.0f); - SetDropShadowPosition(0); - CTxdStore::PopCurrentTxd(); -} - -void -CFont::Shutdown(void) -{ - Sprite[0].Delete(); - Sprite[1].Delete(); - Sprite[2].Delete(); - CTxdStore::RemoveTxdSlot(CTxdStore::FindTxdSlot("fonts")); -} - -void -CFont::InitPerFrame(void) -{ - Details.bank = CSprite2d::GetBank(30, Sprite[0].m_pTexture); - CSprite2d::GetBank(15, Sprite[1].m_pTexture); - CSprite2d::GetBank(15, Sprite[2].m_pTexture); - SetDropShadowPosition(0); - NewLine = 0; -} - -void -CFont::PrintChar(float x, float y, uint16 c) -{ - if(x <= 0.0f || x > SCREEN_WIDTH || - y <= 0.0f || y > SCREEN_HEIGHT) // BUG: game uses SCREENW again - return; - - float w = GetCharacterWidth(c) / 32.0f; - float xoff = c & 0xF; - float yoff = c >> 4; - - if(Details.style == FONT_BANK || Details.style == FONT_HEADING){ - if(Details.dropShadowPosition != 0){ - CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank - CRect(x + SCREEN_SCALE_X(Details.dropShadowPosition), - y + SCREEN_SCALE_Y(Details.dropShadowPosition), - x + SCREEN_SCALE_X(Details.dropShadowPosition) + 32.0f * Details.scaleX * 1.0f, - y + SCREEN_SCALE_Y(Details.dropShadowPosition) + 40.0f * Details.scaleY * 0.5f), - Details.dropColor, - xoff/16.0f, yoff/12.8f, - (xoff+1.0f)/16.0f - 0.001f, yoff/12.8f, - xoff/16.0f, (yoff+1.0f)/12.8f, - (xoff+1.0f)/16.0f - 0.001f, (yoff+1.0f)/12.8f - 0.0001f); - } - CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank - CRect(x, y, - x + 32.0f * Details.scaleX * 1.0f, - y + 40.0f * Details.scaleY * 0.5f), - Details.color, - xoff/16.0f, yoff/12.8f, - (xoff+1.0f)/16.0f - 0.001f, yoff/12.8f, - xoff/16.0f, (yoff+1.0f)/12.8f - 0.002f, - (xoff+1.0f)/16.0f - 0.001f, (yoff+1.0f)/12.8f - 0.002f); - }else{ - CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank - CRect(x, y, - x + 32.0f * Details.scaleX * w, - y + 32.0f * Details.scaleY * 0.5f), - Details.color, - xoff/16.0f, yoff/16.0f, - (xoff+w)/16.0f, yoff/16.0f, - xoff/16.0f, (yoff+1.0f)/16.0f, - (xoff+w)/16.0f - 0.0001f, (yoff+1.0f)/16.0f - 0.0001f); - } -} - -void -CFont::PrintString(float xstart, float ystart, uint16 *s) -{ - CRect rect; - int numSpaces; - float lineLength; - float x, y; - bool first; - uint16 *start, *t; - - if(*s == '*') - return; - - if(Details.background){ - GetNumberLines(xstart, ystart, s); // BUG: result not used - GetTextRect(&rect, xstart, ystart, s); - CSprite2d::DrawRect(rect, Details.backgroundColor); - } - - lineLength = 0.0f; - numSpaces = 0; - first = true; - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - y = ystart; - start = s; - - // This is super ugly, I blame R* - for(;;){ - for(;;){ - for(;;){ - if(*s == '\0') - return; - int xend = Details.centre ? Details.centreSize : - Details.rightJustify ? xstart - Details.rightJustifyWrap : - Details.wrapX; - if(x + GetStringWidth(s) > xend && !first){ - // flush line - float spaceWidth = !Details.justify || Details.centre ? 0.0f : - (Details.wrapX - lineLength) / numSpaces; - float xleft = Details.centre ? xstart - x/2 : - Details.rightJustify ? xstart - x : - xstart; - PrintString(xleft, y, start, s, spaceWidth); - // reset things - lineLength = 0.0f; - numSpaces = 0; - first = true; - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; - start = s; - }else - break; - } - // advance by one word - t = GetNextSpace(s); - if(t[0] == '\0' || - t[0] == ' ' && t[1] == '\0') - break; - if(!first) - numSpaces++; - first = false; - x += GetStringWidth(s) + GetCharacterSize(*t - ' '); - lineLength = x; - s = t+1; - } - // print rest - if(t[0] == ' ' && t[1] == '\0') - t[0] = '\0'; - x += GetStringWidth(s); - s = t; - float xleft = Details.centre ? xstart - x/2 : - Details.rightJustify ? xstart - x : - xstart; - PrintString(xleft, y, start, s, 0.0f); - } -} - -int -CFont::GetNumberLines(float xstart, float ystart, uint16 *s) -{ - int n; - float x, y; - uint16 *t; - - n = 0; - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - y = ystart; - - while(*s){ - if(x + GetStringWidth(s) > (Details.centre ? Details.centreSize : Details.wrapX)){ - // reached end of line - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - n++; - // Why even? - y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; - }else{ - // still space in current line - t = GetNextSpace(s); - if(*t == '\0'){ - // end of string - x += GetStringWidth(s); - n++; - s = t; - }else{ - x += GetStringWidth(s); - x += GetCharacterSize(*t - ' '); - s = t+1; - } - } - } - - return n; -} - -void -CFont::GetTextRect(CRect *rect, float xstart, float ystart, uint16 *s) -{ - int numLines; - float x, y; - int16 maxlength; - uint16 *t; - - maxlength = 0; - numLines = 0; - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - y = ystart; - - while(*s){ - if(x + GetStringWidth(s) > (Details.centre ? Details.centreSize : Details.wrapX)){ - // reached end of line - if(x > maxlength) - maxlength = x; - if(Details.centre || Details.rightJustify) - x = 0.0f; - else - x = xstart; - numLines++; - y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; - }else{ - // still space in current line - t = GetNextSpace(s); - if(*t == '\0'){ - // end of string - x += GetStringWidth(s); - if(x > maxlength) - maxlength = x; - numLines++; - s = t; - }else{ - x += GetStringWidth(s); - x += GetCharacterSize(*t - ' '); - s = t+1; - } - } - } - - if(Details.centre){ - if(Details.backgroundOnlyText){ - rect->left = xstart - maxlength/2 - 4.0f; - rect->right = xstart + maxlength/2 + 4.0f; - rect->bottom = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + - ystart + 2.0f; - rect->top = ystart - 2.0f; - }else{ - rect->left = xstart - Details.centreSize*0.5f - 4.0f; - rect->right = xstart + Details.centreSize*0.5f + 4.0f; - rect->bottom = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + - ystart + 2.0f; - rect->top = ystart - 2.0f; - } - }else{ - rect->left = xstart - 4.0f; - rect->right = Details.wrapX; - // WTF? - rect->bottom = ystart - 4.0f + 4.0f; - rect->top = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + - ystart + 2.0f + 2.0f; - } -} - -void -CFont::PrintString(float x, float y, uint16 *start, uint16 *end, float spwidth) -{ - uint16 *s, c, unused; - - for(s = start; s < end; s++){ - if(*s == '~') - s = ParseToken(s, &unused); - c = *s - ' '; - if(Details.slant != 0.0f) - y = (Details.slantRefX - x)*Details.slant + Details.slantRefY; - PrintChar(x, y, c); - x += GetCharacterSize(c); - if(c == 0) // space - x += spwidth; - } -} - -float -CFont::GetCharacterWidth(uint16 c) -{ - if(Details.proportional) - return Size[Details.style][c]; - else - return Size[Details.style][192]; -} - -float -CFont::GetCharacterSize(uint16 c) -{ - if(Details.proportional) - return Size[Details.style][c] * Details.scaleX; - else - return Size[Details.style][192] * Details.scaleX; -} - -float -CFont::GetStringWidth(uint16 *s, bool spaces) -{ - float w; - - w = 0.0f; - for(; (*s != ' ' || spaces) && *s != '\0'; s++){ - if(*s == '~'){ - s++; - while(*s != '~') s++; - s++; - if(*s == ' ' && !spaces) - break; - } - w += GetCharacterSize(*s - ' '); - } - return w; -} - -uint16* -CFont::GetNextSpace(uint16 *s) -{ - for(; *s != ' ' && *s != '\0'; s++) - if(*s == '~'){ - s++; - while(*s != '~') s++; - s++; - if(*s == ' ') - break; - } - return s; -} - -uint16* -CFont::ParseToken(uint16 *s, uint16*) -{ - s++; - if(Details.color.r || Details.color.g || Details.color.b) - switch(*s){ - case 'N': - case 'n': - NewLine = 1; - break; - case 'b': SetColor(CRGBA(0x80, 0xA7, 0xF3, 0xFF)); break; - case 'g': SetColor(CRGBA(0x5F, 0xA0, 0x6A, 0xFF)); break; - case 'h': SetColor(CRGBA(0xE1, 0xE1, 0xE1, 0xFF)); break; - case 'l': SetColor(CRGBA(0x00, 0x00, 0x00, 0xFF)); break; - case 'p': SetColor(CRGBA(0xA8, 0x6E, 0xFC, 0xFF)); break; - case 'r': SetColor(CRGBA(0x71, 0x2B, 0x49, 0xFF)); break; - case 'w': SetColor(CRGBA(0xAF, 0xAF, 0xAF, 0xFF)); break; - case 'y': SetColor(CRGBA(0xD2, 0xC4, 0x6A, 0xFF)); break; - } - while(*s != '~') s++; - return s+1; -} - -void -CFont::DrawFonts(void) -{ - CSprite2d::DrawBank(Details.bank); - CSprite2d::DrawBank(Details.bank+1); - CSprite2d::DrawBank(Details.bank+2); -} - -uint16 -CFont::character_code(uint8 c) -{ - if(c < 128) - return c; - return foreign_table[c-128]; -} - -STARTPATCHES - - InjectHook(0x500A40, CFont::Initialise, PATCH_JUMP); - InjectHook(0x500BA0, CFont::Shutdown, PATCH_JUMP); - InjectHook(0x500BE0, CFont::InitPerFrame, PATCH_JUMP); - InjectHook(0x500C30, CFont::PrintChar, PATCH_JUMP); - InjectHook(0x500F50, (void (*)(float, float, uint16*))CFont::PrintString, PATCH_JUMP); - InjectHook(0x501260, CFont::GetNumberLines, PATCH_JUMP); - InjectHook(0x5013B0, CFont::GetTextRect, PATCH_JUMP); - InjectHook(0x501730, (void (*)(float, float, uint16*, uint16*, float))CFont::PrintString, PATCH_JUMP); - InjectHook(0x5017E0, CFont::GetCharacterWidth, PATCH_JUMP); - InjectHook(0x501840, CFont::GetCharacterSize, PATCH_JUMP); - InjectHook(0x5018A0, CFont::GetStringWidth, PATCH_JUMP); - InjectHook(0x501960, CFont::GetNextSpace, PATCH_JUMP); - InjectHook(0x5019A0, CFont::ParseToken, PATCH_JUMP); - InjectHook(0x501B50, CFont::DrawFonts, PATCH_JUMP); - InjectHook(0x501E80, CFont::character_code, PATCH_JUMP); - - InjectHook(0x501B80, CFont::SetScale, PATCH_JUMP); - InjectHook(0x501BA0, CFont::SetSlantRefPoint, PATCH_JUMP); - InjectHook(0x501BC0, CFont::SetSlant, PATCH_JUMP); - InjectHook(0x501BD0, CFont::SetColor, PATCH_JUMP); - InjectHook(0x501C60, CFont::SetJustifyOn, PATCH_JUMP); - InjectHook(0x501C80, CFont::SetJustifyOff, PATCH_JUMP); - InjectHook(0x501C90, CFont::SetCentreOn, PATCH_JUMP); - InjectHook(0x501CB0, CFont::SetCentreOff, PATCH_JUMP); - InjectHook(0x501CC0, CFont::SetWrapx, PATCH_JUMP); - InjectHook(0x501CD0, CFont::SetCentreSize, PATCH_JUMP); - InjectHook(0x501CE0, CFont::SetBackgroundOn, PATCH_JUMP); - InjectHook(0x501CF0, CFont::SetBackgroundOff, PATCH_JUMP); - InjectHook(0x501D00, CFont::SetBackgroundColor, PATCH_JUMP); - InjectHook(0x501D30, CFont::SetBackGroundOnlyTextOn, PATCH_JUMP); - InjectHook(0x501D40, CFont::SetBackGroundOnlyTextOff, PATCH_JUMP); - InjectHook(0x501D50, CFont::SetRightJustifyOn, PATCH_JUMP); - InjectHook(0x501D70, CFont::SetRightJustifyOff, PATCH_JUMP); - InjectHook(0x501D90, CFont::SetPropOff, PATCH_JUMP); - InjectHook(0x501DA0, CFont::SetPropOn, PATCH_JUMP); - InjectHook(0x501DB0, CFont::SetFontStyle, PATCH_JUMP); - InjectHook(0x501DC0, CFont::SetRightJustifyWrap, PATCH_JUMP); - InjectHook(0x501DD0, CFont::SetAlphaFade, PATCH_JUMP); - InjectHook(0x501DE0, CFont::SetDropColor, PATCH_JUMP); - InjectHook(0x501E70, CFont::SetDropShadowPosition, PATCH_JUMP); - -ENDPATCHES +#include "common.h" +#include "patcher.h" +#include "Sprite2d.h" +#include "TxdStore.h" +#include "Font.h" + +CFontDetails &CFont::Details = *(CFontDetails*)0x8F317C; +int16 &CFont::NewLine = *(int16*)0x95CC94; +CSprite2d *CFont::Sprite = (CSprite2d*)0x95CC04; + +#ifdef MORE_LANGUAGES +uint8 CFont::LanguageSet = FONT_LANGSET_EFIGS; +int32 CFont::Slot = -1; + +int16 CFont::Size[2][3][193] = { + { +#else +int16 CFont::Size[3][193] = { +#endif + { + 13, 12, 31, 35, 23, 35, 31, 9, 14, 15, 25, 30, 11, 17, 13, 31, + 23, 16, 22, 21, 24, 23, 23, 20, 23, 22, 10, 35, 26, 26, 26, 26, + 30, 26, 24, 23, 24, 22, 21, 24, 26, 10, 20, 26, 22, 29, 26, 25, + 23, 25, 24, 24, 22, 25, 24, 29, 29, 23, 25, 37, 22, 37, 35, 37, + 35, 21, 22, 21, 21, 22, 13, 22, 21, 10, 16, 22, 11, 32, 21, 21, + 23, 22, 16, 20, 14, 21, 20, 30, 25, 21, 21, 33, 33, 33, 33, 35, + 27, 27, 27, 27, 32, 24, 23, 23, 23, 23, 11, 11, 11, 11, 26, 26, + 26, 26, 26, 26, 26, 25, 26, 21, 21, 21, 21, 32, 23, 22, 22, 22, + 22, 11, 11, 11, 11, 22, 22, 22, 22, 22, 22, 22, 22, 26, 21, 24, + 12, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 18, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 20 + }, + + { + 13, 9, 21, 35, 23, 35, 35, 11, 35, 35, 25, 35, 11, 17, 13, 33, + 28, 14, 22, 21, 24, 23, 23, 21, 23, 22, 10, 35, 13, 35, 13, 33, + 5, 25, 22, 23, 24, 21, 21, 24, 24, 9, 20, 24, 21, 27, 25, 25, + 22, 25, 23, 20, 23, 23, 23, 31, 23, 23, 23, 37, 33, 37, 35, 37, + 35, 21, 19, 19, 21, 19, 17, 21, 21, 8, 17, 18, 14, 24, 21, 21, + 20, 22, 19, 20, 20, 19, 20, 26, 21, 20, 21, 33, 33, 33, 33, 35, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 16 + }, + + { + 15, 14, 16, 25, 19, 26, 22, 11, 18, 18, 27, 26, 13, 19, 9, 27, + 19, 18, 19, 19, 22, 19, 20, 18, 19, 20, 12, 32, 15, 32, 15, 35, + 15, 19, 19, 19, 19, 19, 16, 19, 20, 9, 19, 20, 14, 29, 19, 20, + 19, 19, 19, 19, 21, 19, 20, 32, 20, 19, 19, 33, 31, 39, 37, 39, + 37, 21, 21, 21, 23, 21, 19, 23, 23, 10, 19, 20, 16, 26, 23, 23, + 20, 20, 20, 22, 21, 22, 22, 26, 22, 22, 23, 35, 35, 35, 35, 37, + 19, 19, 19, 19, 29, 19, 19, 19, 19, 19, 9, 9, 9, 9, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 30, 19, 19, 19, 19, + 19, 10, 10, 10, 10, 19, 19, 19, 19, 19, 19, 19, 19, 19, 23, 35, + 12, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 11, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19 + } +#ifdef MORE_LANGUAGES + }, + { + { 13, 12, 31, 35, 23, 35, 31, 9, 14, 15, 25, 30, 11, 17, + 13, 31, 23, 16, 22, 21, 24, 23, 23, 20, 23, 22, 10, + 35, 26, 26, 26, 26, 30, 26, 24, 23, 24, 22, 21, 24, + 26, 10, 20, 26, 22, 29, 26, 25, 23, 25, 24, 24, 22, + 25, 24, 29, 29, 23, 25, 37, 22, 37, 35, 37, 35, 21, + 22, 21, 21, 22, 13, 22, 21, 10, 16, 22, 11, 32, 21, + 21, 23, 22, 16, 20, 14, 21, 20, 30, 25, 21, 21, 13, + 33, 13, 13, 13, 24, 22, 22, 19, 26, 21, 30, 20, 23, + 23, 21, 24, 26, 23, 22, 23, 21, 22, 20, 20, 26, 25, + 24, 22, 31, 32, 23, 30, 22, 22, 32, 23, 19, 18, 18, + 15, 22, 19, 27, 19, 20, 20, 18, 22, 24, 20, 19, 19, + 20, 19, 16, 19, 28, 20, 20, 18, 26, 27, 19, 26, 18, + 19, 27, 19, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 18, 26, 26, 26, 26, 26, 26, 26, 26, 26, + 26, 26, 26, 26, 26, 26, 26, 26, 26, 20 }, + { 13, 9, 21, 35, 23, 35, 35, 11, 35, 35, 25, 35, 11, + 17, 13, 33, 28, 14, 22, 21, 24, 23, 23, 21, 23, 22, + 10, 35, 13, 35, 13, 33, 5, 25, 22, 23, 24, 21, 21, 24, + 24, 9, 20, 24, 21, 27, 25, 25, 22, 25, 23, 20, 23, 23, + 23, 31, 23, 23, 23, 37, 33, 37, 35, 37, 35, 21, 19, + 19, 21, 19, 17, 21, 21, 8, 17, 18, 14, 24, 21, 21, 20, + 22, 19, 20, 20, 19, 20, 26, 21, 20, 21, 33, 33, 33, + 33, 35, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 16, }, + { 15, 14, 16, 25, 19, + 26, 22, 11, 18, 18, 27, 26, 13, 19, 9, 27, 19, 18, 19, + 19, 22, 19, 20, 18, 19, 20, 12, 32, 15, 32, 15, 35, + 15, 19, 19, 19, 19, 19, 16, 19, 20, 9, 19, 20, 14, 29, + 19, 20, 19, 19, 19, 19, 21, 19, 20, 32, 20, 19, 19, + 33, 31, 39, 37, 39, 37, 21, 21, 21, 23, 21, 19, 23, 23, 10, 19, 20, 16, 26, 23, + 21, 21, 20, 20, 22, 21, 22, 22, 26, 22, 22, 23, 35, + 35, 35, 35, 37, 19, 19, 19, 19, 19, 19, 29, 19, 19, + 19, 20, 22, 31, 19, 19, 19, 19, 19, 29, 19, 29, 19, + 21, 19, 30, 31, 21, 29, 19, 19, 29, 19, 21, 23, 32, + 21, 21, 30, 31, 22, 21, 32, 33, 23, 32, 21, 21, 32, + 21, 19, 19, 30, 31, 22, 22, 21, 32, 33, 23, 32, 21, + 21, 32, 21, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 11, 19, 19, 19, 19, 19, 19, 19, 19, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19 }, + } +#endif +}; + +uint16 foreign_table[128] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 176, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 177, 0, 0, 0, 0, 0, 95, 0, 0, 0, 0, 175, + 128, 129, 130, 0, 131, 0, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, + 0, 173, 142, 143, 144, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 150, + 151, 152, 153, 0, 154, 0, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 0, 174, 165, 166, 167, 0, 168, 0, 0, 169, 170, 171, 172, 0, 0, 0, +}; + +void +CFont::Initialise(void) +{ + int slot; + + slot = CTxdStore::AddTxdSlot("fonts"); +#ifdef MORE_LANGUAGES + Slot = slot; + switch (LanguageSet) + { + case FONT_LANGSET_EFIGS: + default: + CTxdStore::LoadTxd(slot, "MODELS/FONTS.TXD"); + break; + case FONT_LANGSET_RUSSIAN: + CTxdStore::LoadTxd(slot, "MODELS/FONTS_R.TXD"); + break; + } +#else + CTxdStore::LoadTxd(slot, "MODELS/FONTS.TXD"); +#endif + CTxdStore::AddRef(slot); + CTxdStore::PushCurrentTxd(); + CTxdStore::SetCurrentTxd(slot); + Sprite[0].SetTexture("font2", "font2_mask"); + Sprite[1].SetTexture("pager", "pager_mask"); + Sprite[2].SetTexture("font1", "font1_mask"); + SetScale(1.0f, 1.0f); + SetSlantRefPoint(SCREEN_WIDTH, 0.0f); + SetSlant(0.0f); + SetColor(CRGBA(0xFF, 0xFF, 0xFF, 0)); + SetJustifyOff(); + SetCentreOff(); + SetWrapx(640.0f); + SetCentreSize(640.0f); + SetBackgroundOff(); + SetBackgroundColor(CRGBA(0x80, 0x80, 0x80, 0x80)); + SetBackGroundOnlyTextOff(); + SetPropOn(); + SetFontStyle(FONT_BANK); + SetRightJustifyWrap(0.0f); + SetAlphaFade(255.0f); + SetDropShadowPosition(0); + CTxdStore::PopCurrentTxd(); +} + +#ifdef MORE_LANGUAGES +void +CFont::ReloadFonts(uint8 set) +{ + if (Slot != -1 && LanguageSet != set) { + Sprite[0].Delete(); + Sprite[1].Delete(); + Sprite[2].Delete(); + CTxdStore::PushCurrentTxd(); + CTxdStore::RemoveTxd(Slot); + switch (set) + { + case FONT_LANGSET_EFIGS: + default: + CTxdStore::LoadTxd(Slot, "MODELS/FONTS.TXD"); + break; + case FONT_LANGSET_RUSSIAN: + CTxdStore::LoadTxd(Slot, "MODELS/FONTS_R.TXD"); + break; + } + CTxdStore::SetCurrentTxd(Slot); + Sprite[0].SetTexture("font2", "font2_mask"); + Sprite[1].SetTexture("pager", "pager_mask"); + Sprite[2].SetTexture("font1", "font1_mask"); + CTxdStore::PopCurrentTxd(); + } + LanguageSet = set; +} +#endif + +void +CFont::Shutdown(void) +{ + Sprite[0].Delete(); + Sprite[1].Delete(); + Sprite[2].Delete(); +#ifdef MORE_LANGUAGES + CTxdStore::RemoveTxdSlot(Slot); + Slot = -1; +#else + CTxdStore::RemoveTxdSlot(CTxdStore::FindTxdSlot("fonts")); +#endif +} + +void +CFont::InitPerFrame(void) +{ + Details.bank = CSprite2d::GetBank(30, Sprite[0].m_pTexture); + CSprite2d::GetBank(15, Sprite[1].m_pTexture); + CSprite2d::GetBank(15, Sprite[2].m_pTexture); + SetDropShadowPosition(0); + NewLine = 0; +} + +void +CFont::PrintChar(float x, float y, uint16 c) +{ + if(x <= 0.0f || x > SCREEN_WIDTH || + y <= 0.0f || y > SCREEN_HEIGHT) // BUG: game uses SCREENW again + return; + + float w = GetCharacterWidth(c) / 32.0f; + float xoff = c & 0xF; + float yoff = c >> 4; + + if(Details.style == FONT_BANK || Details.style == FONT_HEADING){ + if(Details.dropShadowPosition != 0){ + CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank + CRect(x + SCREEN_SCALE_X(Details.dropShadowPosition), + y + SCREEN_SCALE_Y(Details.dropShadowPosition), + x + SCREEN_SCALE_X(Details.dropShadowPosition) + 32.0f * Details.scaleX * 1.0f, + y + SCREEN_SCALE_Y(Details.dropShadowPosition) + 40.0f * Details.scaleY * 0.5f), + Details.dropColor, + xoff/16.0f, yoff/12.8f, + (xoff+1.0f)/16.0f - 0.001f, yoff/12.8f, + xoff/16.0f, (yoff+1.0f)/12.8f, + (xoff+1.0f)/16.0f - 0.001f, (yoff+1.0f)/12.8f - 0.0001f); + } + CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank + CRect(x, y, + x + 32.0f * Details.scaleX * 1.0f, + y + 40.0f * Details.scaleY * 0.5f), + Details.color, + xoff/16.0f, yoff/12.8f, + (xoff+1.0f)/16.0f - 0.001f, yoff/12.8f, + xoff/16.0f, (yoff+1.0f)/12.8f - 0.002f, + (xoff+1.0f)/16.0f - 0.001f, (yoff+1.0f)/12.8f - 0.002f); + }else{ + CSprite2d::AddSpriteToBank(Details.bank + Details.style, // BUG: game doesn't add bank + CRect(x, y, + x + 32.0f * Details.scaleX * w, + y + 32.0f * Details.scaleY * 0.5f), + Details.color, + xoff/16.0f, yoff/16.0f, + (xoff+w)/16.0f, yoff/16.0f, + xoff/16.0f, (yoff+1.0f)/16.0f, + (xoff+w)/16.0f - 0.0001f, (yoff+1.0f)/16.0f - 0.0001f); + } +} + +void +CFont::PrintString(float xstart, float ystart, uint16 *s) +{ + CRect rect; + int numSpaces; + float lineLength; + float x, y; + bool first; + uint16 *start, *t; + + if(*s == '*') + return; + + if(Details.background){ + GetNumberLines(xstart, ystart, s); // BUG: result not used + GetTextRect(&rect, xstart, ystart, s); + CSprite2d::DrawRect(rect, Details.backgroundColor); + } + + lineLength = 0.0f; + numSpaces = 0; + first = true; + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + y = ystart; + start = s; + + // This is super ugly, I blame R* + for(;;){ + for(;;){ + for(;;){ + if(*s == '\0') + return; + int xend = Details.centre ? Details.centreSize : + Details.rightJustify ? xstart - Details.rightJustifyWrap : + Details.wrapX; + if(x + GetStringWidth(s) > xend && !first){ + // flush line + float spaceWidth = !Details.justify || Details.centre ? 0.0f : + (Details.wrapX - lineLength) / numSpaces; + float xleft = Details.centre ? xstart - x/2 : + Details.rightJustify ? xstart - x : + xstart; + PrintString(xleft, y, start, s, spaceWidth); + // reset things + lineLength = 0.0f; + numSpaces = 0; + first = true; + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; + start = s; + }else + break; + } + // advance by one word + t = GetNextSpace(s); + if(t[0] == '\0' || + t[0] == ' ' && t[1] == '\0') + break; + if(!first) + numSpaces++; + first = false; + x += GetStringWidth(s) + GetCharacterSize(*t - ' '); + lineLength = x; + s = t+1; + } + // print rest + if(t[0] == ' ' && t[1] == '\0') + t[0] = '\0'; + x += GetStringWidth(s); + s = t; + float xleft = Details.centre ? xstart - x/2 : + Details.rightJustify ? xstart - x : + xstart; + PrintString(xleft, y, start, s, 0.0f); + } +} + +int +CFont::GetNumberLines(float xstart, float ystart, uint16 *s) +{ + int n; + float x, y; + uint16 *t; + + n = 0; + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + y = ystart; + + while(*s){ + if(x + GetStringWidth(s) > (Details.centre ? Details.centreSize : Details.wrapX)){ + // reached end of line + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + n++; + // Why even? + y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; + }else{ + // still space in current line + t = GetNextSpace(s); + if(*t == '\0'){ + // end of string + x += GetStringWidth(s); + n++; + s = t; + }else{ + x += GetStringWidth(s); + x += GetCharacterSize(*t - ' '); + s = t+1; + } + } + } + + return n; +} + +void +CFont::GetTextRect(CRect *rect, float xstart, float ystart, uint16 *s) +{ + int numLines; + float x, y; + int16 maxlength; + uint16 *t; + + maxlength = 0; + numLines = 0; + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + y = ystart; + + while(*s){ + if(x + GetStringWidth(s) > (Details.centre ? Details.centreSize : Details.wrapX)){ + // reached end of line + if(x > maxlength) + maxlength = x; + if(Details.centre || Details.rightJustify) + x = 0.0f; + else + x = xstart; + numLines++; + y += 32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY; + }else{ + // still space in current line + t = GetNextSpace(s); + if(*t == '\0'){ + // end of string + x += GetStringWidth(s); + if(x > maxlength) + maxlength = x; + numLines++; + s = t; + }else{ + x += GetStringWidth(s); + x += GetCharacterSize(*t - ' '); + s = t+1; + } + } + } + + if(Details.centre){ + if(Details.backgroundOnlyText){ + rect->left = xstart - maxlength/2 - 4.0f; + rect->right = xstart + maxlength/2 + 4.0f; + rect->bottom = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + + ystart + 2.0f; + rect->top = ystart - 2.0f; + }else{ + rect->left = xstart - Details.centreSize*0.5f - 4.0f; + rect->right = xstart + Details.centreSize*0.5f + 4.0f; + rect->bottom = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + + ystart + 2.0f; + rect->top = ystart - 2.0f; + } + }else{ + rect->left = xstart - 4.0f; + rect->right = Details.wrapX; + // WTF? + rect->bottom = ystart - 4.0f + 4.0f; + rect->top = (32.0f * CFont::Details.scaleY * 0.5f + 2.0f * CFont::Details.scaleY) * numLines + + ystart + 2.0f + 2.0f; + } +} + +void +CFont::PrintString(float x, float y, uint16 *start, uint16 *end, float spwidth) +{ + uint16 *s, c, unused; + + for(s = start; s < end; s++){ + if(*s == '~') + s = ParseToken(s, &unused); + c = *s - ' '; + if(Details.slant != 0.0f) + y = (Details.slantRefX - x)*Details.slant + Details.slantRefY; + PrintChar(x, y, c); + x += GetCharacterSize(c); + if(c == 0) // space + x += spwidth; + } +} + +float +CFont::GetCharacterWidth(uint16 c) +{ +#ifdef MORE_LANGUAGES + if (Details.proportional) + return Size[LanguageSet][Details.style][c]; + else + return Size[LanguageSet][Details.style][192]; +#else + if (Details.proportional) + return Size[Details.style][c]; + else + return Size[Details.style][192]; +#endif // MORE_LANGUAGES +} + +float +CFont::GetCharacterSize(uint16 c) +{ +#ifdef MORE_LANGUAGES + if(Details.proportional) + return Size[LanguageSet][Details.style][c] * Details.scaleX; + else + return Size[LanguageSet][Details.style][192] * Details.scaleX; +#else + if (Details.proportional) + return Size[Details.style][c] * Details.scaleX; + else + return Size[Details.style][192] * Details.scaleX; +#endif // MORE_LANGUAGES +} + +float +CFont::GetStringWidth(uint16 *s, bool spaces) +{ + float w; + + w = 0.0f; + for(; (*s != ' ' || spaces) && *s != '\0'; s++){ + if(*s == '~'){ + s++; + while(*s != '~') s++; + s++; + if(*s == ' ' && !spaces) + break; + } + w += GetCharacterSize(*s - ' '); + } + return w; +} + +uint16* +CFont::GetNextSpace(uint16 *s) +{ + for(; *s != ' ' && *s != '\0'; s++) + if(*s == '~'){ + s++; + while(*s != '~') s++; + s++; + if(*s == ' ') + break; + } + return s; +} + +uint16* +CFont::ParseToken(uint16 *s, uint16*) +{ + s++; + if(Details.color.r || Details.color.g || Details.color.b) + switch(*s){ + case 'N': + case 'n': + NewLine = 1; + break; + case 'b': SetColor(CRGBA(0x80, 0xA7, 0xF3, 0xFF)); break; + case 'g': SetColor(CRGBA(0x5F, 0xA0, 0x6A, 0xFF)); break; + case 'h': SetColor(CRGBA(0xE1, 0xE1, 0xE1, 0xFF)); break; + case 'l': SetColor(CRGBA(0x00, 0x00, 0x00, 0xFF)); break; + case 'p': SetColor(CRGBA(0xA8, 0x6E, 0xFC, 0xFF)); break; + case 'r': SetColor(CRGBA(0x71, 0x2B, 0x49, 0xFF)); break; + case 'w': SetColor(CRGBA(0xAF, 0xAF, 0xAF, 0xFF)); break; + case 'y': SetColor(CRGBA(0xD2, 0xC4, 0x6A, 0xFF)); break; + } + while(*s != '~') s++; + return s+1; +} + +void +CFont::DrawFonts(void) +{ + CSprite2d::DrawBank(Details.bank); + CSprite2d::DrawBank(Details.bank+1); + CSprite2d::DrawBank(Details.bank+2); +} + +uint16 +CFont::character_code(uint8 c) +{ + if(c < 128) + return c; + return foreign_table[c-128]; +} + +STARTPATCHES + + InjectHook(0x500A40, CFont::Initialise, PATCH_JUMP); + InjectHook(0x500BA0, CFont::Shutdown, PATCH_JUMP); + InjectHook(0x500BE0, CFont::InitPerFrame, PATCH_JUMP); + InjectHook(0x500C30, CFont::PrintChar, PATCH_JUMP); + InjectHook(0x500F50, (void (*)(float, float, uint16*))CFont::PrintString, PATCH_JUMP); + InjectHook(0x501260, CFont::GetNumberLines, PATCH_JUMP); + InjectHook(0x5013B0, CFont::GetTextRect, PATCH_JUMP); + InjectHook(0x501730, (void (*)(float, float, uint16*, uint16*, float))CFont::PrintString, PATCH_JUMP); + InjectHook(0x5017E0, CFont::GetCharacterWidth, PATCH_JUMP); + InjectHook(0x501840, CFont::GetCharacterSize, PATCH_JUMP); + InjectHook(0x5018A0, CFont::GetStringWidth, PATCH_JUMP); + InjectHook(0x501960, CFont::GetNextSpace, PATCH_JUMP); + InjectHook(0x5019A0, CFont::ParseToken, PATCH_JUMP); + InjectHook(0x501B50, CFont::DrawFonts, PATCH_JUMP); + InjectHook(0x501E80, CFont::character_code, PATCH_JUMP); + + InjectHook(0x501B80, CFont::SetScale, PATCH_JUMP); + InjectHook(0x501BA0, CFont::SetSlantRefPoint, PATCH_JUMP); + InjectHook(0x501BC0, CFont::SetSlant, PATCH_JUMP); + InjectHook(0x501BD0, CFont::SetColor, PATCH_JUMP); + InjectHook(0x501C60, CFont::SetJustifyOn, PATCH_JUMP); + InjectHook(0x501C80, CFont::SetJustifyOff, PATCH_JUMP); + InjectHook(0x501C90, CFont::SetCentreOn, PATCH_JUMP); + InjectHook(0x501CB0, CFont::SetCentreOff, PATCH_JUMP); + InjectHook(0x501CC0, CFont::SetWrapx, PATCH_JUMP); + InjectHook(0x501CD0, CFont::SetCentreSize, PATCH_JUMP); + InjectHook(0x501CE0, CFont::SetBackgroundOn, PATCH_JUMP); + InjectHook(0x501CF0, CFont::SetBackgroundOff, PATCH_JUMP); + InjectHook(0x501D00, CFont::SetBackgroundColor, PATCH_JUMP); + InjectHook(0x501D30, CFont::SetBackGroundOnlyTextOn, PATCH_JUMP); + InjectHook(0x501D40, CFont::SetBackGroundOnlyTextOff, PATCH_JUMP); + InjectHook(0x501D50, CFont::SetRightJustifyOn, PATCH_JUMP); + InjectHook(0x501D70, CFont::SetRightJustifyOff, PATCH_JUMP); + InjectHook(0x501D90, CFont::SetPropOff, PATCH_JUMP); + InjectHook(0x501DA0, CFont::SetPropOn, PATCH_JUMP); + InjectHook(0x501DB0, CFont::SetFontStyle, PATCH_JUMP); + InjectHook(0x501DC0, CFont::SetRightJustifyWrap, PATCH_JUMP); + InjectHook(0x501DD0, CFont::SetAlphaFade, PATCH_JUMP); + InjectHook(0x501DE0, CFont::SetDropColor, PATCH_JUMP); + InjectHook(0x501E70, CFont::SetDropShadowPosition, PATCH_JUMP); + +ENDPATCHES diff --git a/src/render/Font.h b/src/render/Font.h index 132ad168..26309377 100644 --- a/src/render/Font.h +++ b/src/render/Font.h @@ -39,9 +39,23 @@ enum { ALIGN_RIGHT, }; +#ifdef MORE_LANGUAGES +enum +{ + FONT_LANGSET_EFIGS, + FONT_LANGSET_RUSSIAN +}; +#endif + class CFont { +#ifdef MORE_LANGUAGES + static int16 Size[2][3][193]; + static uint8 LanguageSet; + static int32 Slot; +#else static int16 Size[3][193]; +#endif static int16 static CSprite2d *Sprite; //[3] public: @@ -136,4 +150,6 @@ public: if(Details.alphaFade < 255.0f) Details.dropColor.a *= Details.alphaFade/255.0f; } + + static void ReloadFonts(uint8 set); }; diff --git a/src/text/Text.cpp b/src/text/Text.cpp index 8bffa7e1..d0cdb310 100644 --- a/src/text/Text.cpp +++ b/src/text/Text.cpp @@ -43,6 +43,11 @@ CText::Load(void) case LANGUAGE_SPANISH: sprintf(filename, "SPANISH.GXT"); break; +#ifdef MORE_LANGUAGES + case LANGUAGE_RUSSIAN: + sprintf(filename, "RUSSIAN.GXT"); + break; +#endif } length = CFileMgr::LoadFile(filename, filedata, 0x40000, "rb"); From 50b43b680e34f42e33fc29ddbdb060b9c2266de2 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Tue, 31 Mar 2020 00:01:03 +0300 Subject: [PATCH 54/70] finished garages --- src/control/Garages.cpp | 584 +++++++++++++++++++++++++++++++++++++--- src/control/Garages.h | 15 +- 2 files changed, 550 insertions(+), 49 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index a89f5337..f7211272 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -3,6 +3,9 @@ #include "Garages.h" #include "main.h" +#ifdef FIX_BUGS +#include "Boat.h" +#endif #include "DMAudio.h" #include "General.h" #include "Font.h" @@ -15,6 +18,7 @@ #include "PlayerPed.h" #include "Replay.h" #include "Stats.h" +#include "Streaming.h" #include "Text.h" #include "Timer.h" #include "Vehicle.h" @@ -53,6 +57,8 @@ #define DISTANCE_TO_OPEN_HIDEOUT_GARAGE_IN_CAR (10.0f) #define DISTANCE_TO_SHOW_HIDEOUT_MESSAGE (5.0f) +#define DISTANCE_TO_CONSIDER_DOOR_FOR_GARAGE (20.0f) + // Time #define TIME_TO_RESPRAY (2000) #define TIME_TO_SETUP_BOMB (2000) @@ -62,6 +68,7 @@ // Respray stuff #define FREE_RESPRAY_HEALTH_THRESHOLD (970.0f) #define NUM_PARTICLES_IN_RESPRAY (200) +#define RESPRAY_CENTERING_COEFFICIENT (0.75f) // Bomb stuff #define KGS_OF_EXPLOSIVES_IN_BOMB (10) @@ -87,9 +94,16 @@ #define MAX_STORED_CARS_IN_INDUSTRIAL (1) #define MAX_STORED_CARS_IN_COMMERCIAL (NUM_GARAGE_STORED_CARS) #define MAX_STORED_CARS_IN_SUBURBAN (NUM_GARAGE_STORED_CARS) +#define LIMIT_CARS_IN_INDUSTRIAL (1) +#define LIMIT_CARS_IN_COMMERCIAL (2) +#define LIMIT_CARS_IN_SUBURBAN (3) #define HIDEOUT_DOOR_SPEED_COEFFICIENT (1.7f) #define TIME_BETWEEN_HIDEOUT_MESSAGES (18000) +// Camera stuff +#define MARGIN_FOR_CAMERA_COLLECTCARS (1.3f) +#define MARGIN_FOR_CAMERA_DEFAULT (4.0f) + const int32 gaCarsToCollectInCraigsGarages[TOTAL_COLLECTCARS_GARAGES][TOTAL_COLLECTCARS_CARS] = { { MI_SECURICA, MI_MOONBEAM, MI_COACH, MI_FLATBED, MI_LINERUN, MI_TRASH, MI_PATRIOT, MI_MRWHOOP, MI_BLISTA, MI_MULE, MI_YANKEE, MI_BOBCAT, MI_DODO, MI_BUS, MI_RUMPO, MI_PONY }, @@ -180,7 +194,7 @@ void CGarages::Update(void) GarageToBeTidied = 0; if (!aGarages[GarageToBeTidied].IsUsed()) return; - if (aGarages[GarageToBeTidied].IsClose()) + if (!aGarages[GarageToBeTidied].IsFar()) aGarages[GarageToBeTidied].TidyUpGarageClose(); else aGarages[GarageToBeTidied].TidyUpGarage(); @@ -222,9 +236,9 @@ int16 CGarages::AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z pGarage->m_fDoorPos = 0.0f; pGarage->m_eGarageState = GS_FULLYCLOSED; pGarage->m_nTimeToStartAction = 0; - pGarage->field_2 = 0; + pGarage->field_2 = false; pGarage->m_nTargetModelIndex = targetId; - pGarage->field_96 = 0; + pGarage->field_96 = nil; pGarage->m_bCollectedCarsState = 0; pGarage->m_bDeactivated = false; pGarage->m_bResprayHappened = false; @@ -1405,18 +1419,13 @@ void CGarage::RemoveCarsBlockingDoorNotInside() continue; if (!IsEntityTouching3D(pVehicle)) continue; - CColModel* pColModel = CModelInfo::GetModelInfo(pVehicle->GetModelIndex())->GetColModel(); - for (int i = 0; i < pColModel->numSpheres; i++) { - CVector pos = pVehicle->GetMatrix() * pColModel->spheres[i].center; - float radius = pColModel->spheres[i].radius; - if (pVehicle->GetPosition().x < m_fX1 || pVehicle->GetPosition().x > m_fX2 || - pVehicle->GetPosition().y < m_fY1 || pVehicle->GetPosition().y > m_fY2 || - pVehicle->GetPosition().z < m_fZ1 || pVehicle->GetPosition().z > m_fZ2) { - if (pVehicle->bIsLocked && pVehicle->CanBeDeleted()) { - CWorld::Remove(pVehicle); - delete pVehicle; - return; // WHY? - } + if (pVehicle->GetPosition().x < m_fX1 || pVehicle->GetPosition().x > m_fX2 || + pVehicle->GetPosition().y < m_fY1 || pVehicle->GetPosition().y > m_fY2 || + pVehicle->GetPosition().z < m_fZ1 || pVehicle->GetPosition().z > m_fZ2) { + if (pVehicle->bIsLocked && pVehicle->CanBeDeleted()) { + CWorld::Remove(pVehicle); + delete pVehicle; + return; // WHY? } } } @@ -1566,6 +1575,7 @@ void CGarage::RefreshDoorPointers(bool bCreate) bool bNeedToFindDoorEntities = true; if (!bCreate && !m_bRecreateDoorOnNextRefresh) bNeedToFindDoorEntities = false; + m_bRecreateDoorOnNextRefresh = false; if (DoINeedToRefreshPointer(m_pDoor1, m_bDoor1IsDummy, m_bDoor1PoolIndex)) bNeedToFindDoorEntities = true; if (DoINeedToRefreshPointer(m_pDoor2, m_bDoor2IsDummy, m_bDoor2PoolIndex)) @@ -1775,7 +1785,58 @@ void CGarage::FindDoorsEntities() } } -WRAPPER void CGarage::FindDoorsEntitiesSectorList(CPtrList&, bool) { EAXJMP(0x427300); } +void CGarage::FindDoorsEntitiesSectorList(CPtrList& list, bool dummy) +{ + CPtrNode* node; + for (node = list.first; node; node = node->next) { + CEntity* pEntity = (CEntity*)node->item; + if (pEntity->m_scanCode == CWorld::GetCurrentScanCode()) + continue; + pEntity->m_scanCode = CWorld::GetCurrentScanCode(); + if (!pEntity || !CGarages::IsModelIndexADoor(pEntity->GetModelIndex())) + continue; + if (Abs(pEntity->GetPosition().x - GetGarageCenterX()) >= DISTANCE_TO_CONSIDER_DOOR_FOR_GARAGE) + continue; + if (Abs(pEntity->GetPosition().y - GetGarageCenterY()) >= DISTANCE_TO_CONSIDER_DOOR_FOR_GARAGE) + continue; + if (pEntity->GetModelIndex() == MI_CRUSHERBODY) { + m_pDoor1 = pEntity; + m_bDoor1IsDummy = dummy; + // very odd pool operations, they could have used GetJustIndex + if (dummy) + m_bDoor1PoolIndex = (CPools::GetDummyPool()->GetIndex((CDummy*)pEntity)) & 0x7F; + else + m_bDoor1PoolIndex = (CPools::GetObjectPool()->GetIndex((CObject*)pEntity)) & 0x7F; + continue; + } + if (pEntity->GetModelIndex() == MI_CRUSHERLID) { + m_pDoor2 = pEntity; + m_bDoor2IsDummy = dummy; + if (dummy) + m_bDoor2PoolIndex = (CPools::GetDummyPool()->GetIndex((CDummy*)pEntity)) & 0x7F; + else + m_bDoor2PoolIndex = (CPools::GetObjectPool()->GetIndex((CObject*)pEntity)) & 0x7F; + continue; + } + if (!m_pDoor1) { + m_pDoor1 = pEntity; + m_bDoor1IsDummy = dummy; + if (dummy) + m_bDoor1PoolIndex = (CPools::GetDummyPool()->GetIndex((CDummy*)pEntity)) & 0x7F; + else + m_bDoor1PoolIndex = (CPools::GetObjectPool()->GetIndex((CObject*)pEntity)) & 0x7F; + continue; + } + else { + m_pDoor2 = pEntity; + m_bDoor2IsDummy = dummy; + if (dummy) + m_bDoor2PoolIndex = (CPools::GetDummyPool()->GetIndex((CDummy*)pEntity)) & 0x7F; + else + m_bDoor2PoolIndex = (CPools::GetObjectPool()->GetIndex((CObject*)pEntity)) & 0x7F; + } + } +} bool CGarages::HasResprayHappened(int16 garage) { @@ -1805,12 +1866,139 @@ bool CGarages::HasCarBeenCrushed(int32 handle) return CrushedCarId == handle; } -WRAPPER void CStoredCar::StoreCar(CVehicle*) { EAXJMP(0x4275C0); } -WRAPPER CVehicle* CStoredCar::RestoreCar() { EAXJMP(0x427690); } -WRAPPER void CGarage::StoreAndRemoveCarsForThisHideout(CStoredCar*, int32) { EAXJMP(0x427840); } -WRAPPER bool CGarage::RestoreCarsForThisHideout(CStoredCar*) { EAXJMP(0x427A40); } -WRAPPER bool CGarages::IsPointInAGarageCameraZone(CVector) { EAXJMP(0x427AB0); } -WRAPPER bool CGarages::CameraShouldBeOutside() { EAXJMP(0x427BC0); } +void CStoredCar::StoreCar(CVehicle* pVehicle) +{ + m_nModelIndex = pVehicle->GetModelIndex(); + m_vecPos = pVehicle->GetPosition(); + m_vecAngle = pVehicle->GetForward(); + m_nPrimaryColor = pVehicle->m_currentColour1; + m_nSecondaryColor = pVehicle->m_currentColour2; + m_nRadioStation = pVehicle->m_nRadioStation; + m_nVariationA = pVehicle->m_aExtras[0]; + m_nVariationB = pVehicle->m_aExtras[1]; + m_bBulletproof = pVehicle->bBulletProof; + m_bFireproof = pVehicle->bFireProof; + m_bExplosionproof = pVehicle->bExplosionProof; + m_bCollisionproof = pVehicle->bCollisionProof; + m_bMeleeproof = pVehicle->bMeleeProof; + if (pVehicle->IsCar()) + m_nCarBombType = ((CAutomobile*)pVehicle)->m_bombType; +} + +CVehicle* CStoredCar::RestoreCar() +{ + CStreaming::RequestModel(m_nModelIndex, STREAMFLAGS_DEPENDENCY); + if (!CStreaming::HasModelLoaded(m_nModelIndex)) + return nil; + CVehicleModelInfo::SetComponentsToUse(m_nVariationA, m_nVariationB); +#ifdef FIX_BUGS + CVehicle* pVehicle; + if (CModelInfo::IsBoatModel(m_nModelIndex)) + pVehicle = new CBoat(m_nModelIndex, RANDOM_VEHICLE); + else + pVehicle = new CAutomobile(m_nModelIndex, RANDOM_VEHICLE); +#else + CVehicle* pVehicle = new CAutomobile(m_nModelIndex, RANDOM_VEHICLE); +#endif + pVehicle->GetPosition() = m_vecPos; + pVehicle->m_status = STATUS_ABANDONED; + pVehicle->GetForward() = m_vecAngle; + pVehicle->GetRight() = CVector(m_vecAngle.y, -m_vecAngle.x, 0.0f); + pVehicle->GetUp() = CVector(0.0f, 0.0f, 1.0f); + pVehicle->pDriver = nil; + pVehicle->m_currentColour1 = m_nPrimaryColor; + pVehicle->m_currentColour2 = m_nSecondaryColor; + pVehicle->m_nRadioStation = m_nRadioStation; + pVehicle->bFreebies = false; +#ifdef FIX_BUGS + ((CAutomobile*)pVehicle)->m_bombType = m_nCarBombType; +#endif + pVehicle->bHasBeenOwnedByPlayer = true; + pVehicle->m_nDoorLock = CARLOCK_UNLOCKED; + pVehicle->bBulletProof = m_bBulletproof; + pVehicle->bFireProof = m_bFireproof; + pVehicle->bExplosionProof = m_bExplosionproof; + pVehicle->bCollisionProof = m_bCollisionproof; + pVehicle->bMeleeProof = m_bMeleeproof; + return pVehicle; +} + +void CGarage::StoreAndRemoveCarsForThisHideout(CStoredCar* aCars, int32 nMax) +{ + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) + aCars[i].Clear(); + int i = CPools::GetVehiclePool()->GetSize(); + int index = 0; + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle) + continue; + if (pVehicle->GetPosition().x > m_fX1 && pVehicle->GetPosition().x < m_fX2 && + pVehicle->GetPosition().y > m_fY1 && pVehicle->GetPosition().y < m_fY2 && + pVehicle->GetPosition().z > m_fZ1 && pVehicle->GetPosition().z < m_fZ2) { + if (pVehicle->VehicleCreatedBy != MISSION_VEHICLE) { + if (index < max(NUM_GARAGE_STORED_CARS, nMax) && !EntityHasASphereWayOutsideGarage(pVehicle, 1.0f)) + aCars[index++].StoreCar(pVehicle); + CWorld::Players[CWorld::PlayerInFocus].CancelPlayerEnteringCars(pVehicle); + CWorld::Remove(pVehicle); + delete pVehicle; + } + } + } + // why? + for (i = index; i < NUM_GARAGE_STORED_CARS; i++) + aCars[i].Clear(); +} + +bool CGarage::RestoreCarsForThisHideout(CStoredCar* aCars) +{ + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) { + if (aCars[i].HasCar()) { + CVehicle* pVehicle = aCars[i].RestoreCar(); + if (pVehicle) { + CWorld::Add(pVehicle); + aCars[i].Clear(); + } + } + } + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) { + if (aCars[i].HasCar()) + return false; + } + return true; +} + +bool CGarages::IsPointInAGarageCameraZone(CVector point) +{ + for (int i = 0; i < NUM_GARAGES; i++) { + switch (aGarages[i].m_eGarageType) { + case GARAGE_NONE: + continue; + case GARAGE_COLLECTCARS_1: + case GARAGE_COLLECTCARS_2: + case GARAGE_COLLECTCARS_3: + if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_COLLECTCARS < point.x || + aGarages[i].m_fX2 - MARGIN_FOR_CAMERA_COLLECTCARS > point.x || + aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_COLLECTCARS < point.y || + aGarages[i].m_fY2 - MARGIN_FOR_CAMERA_COLLECTCARS > point.y) + continue; + return true; + default: + if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_DEFAULT < point.x || + aGarages[i].m_fX2 - MARGIN_FOR_CAMERA_DEFAULT > point.x || + aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_DEFAULT < point.y || + aGarages[i].m_fY2 - MARGIN_FOR_CAMERA_DEFAULT > point.y) + continue; + return true; + } + } + return false; +} + +bool CGarages::CameraShouldBeOutside() +{ + return bCamShouldBeOutisde; +} void CGarages::GivePlayerDetonator() { @@ -1818,19 +2006,299 @@ void CGarages::GivePlayerDetonator() FindPlayerPed()->GetWeapon(FindPlayerPed()->GetWeaponSlot(WEAPONTYPE_DETONATOR)).m_eWeaponState = WEAPONSTATE_READY; } -WRAPPER float CGarages::FindDoorHeightForMI(int32) { EAXJMP(0x427C10); } -WRAPPER void CGarage::TidyUpGarage() { EAXJMP(0x427C30); } -WRAPPER void CGarage::TidyUpGarageClose() { EAXJMP(0x427D90); } -WRAPPER void CGarages::PlayerArrestedOrDied() { EAXJMP(0x427F60); } -WRAPPER void CGarage::PlayerArrestedOrDied() { EAXJMP(0x427FC0); } -WRAPPER void CGarage::CenterCarInGarage(CVehicle*) { EAXJMP(0x428000); } -WRAPPER void CGarages::CloseHideOutGaragesBeforeSave() { EAXJMP(0x428130); } -WRAPPER int32 CGarages::CountCarsInHideoutGarage(eGarageType) { EAXJMP(0x4281E0); } -WRAPPER int32 CGarages::FindMaxNumStoredCarsForGarage(eGarageType) { EAXJMP(0x428230); } -WRAPPER bool CGarages::IsPointWithinHideOutGarage(CVector&) { EAXJMP(0x428260); } -WRAPPER bool CGarages::IsPointWithinAnyGarage(CVector&) { EAXJMP(0x428320); } -WRAPPER void CGarages::SetAllDoorsBackToOriginalHeight() { EAXJMP(0x4283D0); } -WRAPPER void CGarages::Save(uint8 * buf, uint32 * size) { EAXJMP(0x4284E0); } +float CGarages::FindDoorHeightForMI(int32 mi) +{ + return CModelInfo::GetModelInfo(mi)->GetColModel()->boundingBox.max.z - CModelInfo::GetModelInfo(mi)->GetColModel()->boundingBox.min.z - 0.1f; +} + +void CGarage::TidyUpGarage() +{ + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle || !pVehicle->IsCar()) + continue; + if (pVehicle->GetPosition().x > m_fX1 && pVehicle->GetPosition().x < m_fX2 && + pVehicle->GetPosition().y > m_fY1 && pVehicle->GetPosition().y < m_fY2 && + pVehicle->GetPosition().z > m_fZ1 && pVehicle->GetPosition().z < m_fZ2) { + if (pVehicle->m_status == STATUS_WRECKED || pVehicle->GetUp().z < 0.5f) { + CWorld::Remove(pVehicle); + delete pVehicle; + } + } + } +} + +void CGarage::TidyUpGarageClose() +{ + uint32 i = CPools::GetVehiclePool()->GetSize(); + while (i--) { + CVehicle* pVehicle = CPools::GetVehiclePool()->GetSlot(i); + if (!pVehicle || !pVehicle->IsCar()) + continue; + if (!pVehicle->IsCar() || pVehicle->m_status != STATUS_WRECKED || !IsEntityTouching3D(pVehicle)) + continue; + bool bRemove = false; + if (m_eGarageState != GS_FULLYCLOSED) { + CColModel* pColModel = CModelInfo::GetModelInfo(pVehicle->GetModelIndex())->GetColModel(); + for (int i = 0; i < pColModel->numSpheres; i++) { + CVector pos = pVehicle->GetMatrix() * pColModel->spheres[i].center; + float radius = pColModel->spheres[i].radius; + if (pos.x + radius < m_fX1 || pos.x - radius > m_fX2 || + pos.y + radius < m_fY1 || pos.y - radius > m_fY2 || + pos.z + radius < m_fZ1 || pos.z - radius > m_fZ2) { + bRemove = true; + } + } + } + else + bRemove = true; + if (bRemove) { + // no MISSION_VEHICLE check??? + CWorld::Remove(pVehicle); + delete pVehicle; + } + } +} + +void CGarages::PlayerArrestedOrDied() +{ + static int GarageToBeTidied = 0; // lol + for (int i = 0; i < NUM_GARAGES; i++) { + if (aGarages[i].m_eGarageType != GARAGE_NONE) + aGarages[i].PlayerArrestedOrDied(); + } + MessageEndTime = 0; + MessageStartTime = 0; +} + +void CGarage::PlayerArrestedOrDied() +{ + switch (m_eGarageType) { + case GARAGE_MISSION: + case GARAGE_COLLECTORSITEMS: + case GARAGE_COLLECTSPECIFICCARS: + case GARAGE_COLLECTCARS_1: + case GARAGE_COLLECTCARS_2: + case GARAGE_COLLECTCARS_3: + case GARAGE_FORCARTOCOMEOUTOF: + case GARAGE_60SECONDS: + case GARAGE_MISSION_KEEPCAR: + case GARAGE_FOR_SCRIPT_TO_OPEN: + case GARAGE_HIDEOUT_ONE: + case GARAGE_HIDEOUT_TWO: + case GARAGE_HIDEOUT_THREE: + case GARAGE_FOR_SCRIPT_TO_OPEN_AND_CLOSE: + case GARAGE_KEEPS_OPENING_FOR_SPECIFIC_CAR: + case GARAGE_MISSION_KEEPCAR_REMAINCLOSED: + switch (m_eGarageState) { + case GS_OPENED: + case GS_CLOSING: + case GS_OPENING: + m_eGarageState = GS_CLOSING; + break; + default: + break; + } + break; + case GARAGE_BOMBSHOP1: + case GARAGE_BOMBSHOP2: + case GARAGE_BOMBSHOP3: + case GARAGE_RESPRAY: + case GARAGE_CRUSHER: + switch (m_eGarageState) { + case GS_FULLYCLOSED: + case GS_CLOSING: + case GS_OPENING: + m_eGarageState = GS_OPENING; + break; + default: + break; + } + break; + default: + break; + } +} + +void CGarage::CenterCarInGarage(CVehicle* pVehicle) +{ + if (IsAnyOtherCarTouchingGarage(FindPlayerVehicle())) + return; + if (IsAnyOtherPedTouchingGarage(FindPlayerPed())) + return; + float posX = pVehicle->GetPosition().x; + float posY = pVehicle->GetPosition().y; + float posZ = pVehicle->GetPosition().z; + float garageX = GetGarageCenterX(); + float garageY = GetGarageCenterY(); + float offsetX = garageX - posX; + float offsetY = garageY - posY; + float offsetZ = posZ - posZ; + float distance = CVector(offsetX, offsetY, offsetZ).Magnitude(); + if (distance < RESPRAY_CENTERING_COEFFICIENT) { + pVehicle->GetPosition().x = GetGarageCenterX(); + pVehicle->GetPosition().y = GetGarageCenterY(); + } + else { + pVehicle->GetPosition().x += offsetX * RESPRAY_CENTERING_COEFFICIENT / distance; + pVehicle->GetPosition().y += offsetY * RESPRAY_CENTERING_COEFFICIENT / distance; + } + if (!IsEntityEntirelyInside3D(pVehicle, 0.1f)) + pVehicle->GetPosition() = CVector(posX, posY, posZ); +} + +void CGarages::CloseHideOutGaragesBeforeSave() +{ + for (int i = 0; i < NUM_GARAGES; i++) { + if (aGarages[i].m_eGarageType != GARAGE_HIDEOUT_ONE && + aGarages[i].m_eGarageType != GARAGE_HIDEOUT_TWO && + aGarages[i].m_eGarageType != GARAGE_HIDEOUT_THREE) + continue; + if (aGarages[i].m_eGarageState != GS_FULLYCLOSED && + aGarages[i].m_eGarageType != GARAGE_HIDEOUT_ONE || !aGarages[i].IsAnyCarBlockingDoor()) { + aGarages[i].m_eGarageState = GS_FULLYCLOSED; + switch (aGarages[i].m_eGarageType) { + case GARAGE_HIDEOUT_ONE: + aGarages[i].StoreAndRemoveCarsForThisHideout(aCarsInSafeHouse1, NUM_GARAGE_STORED_CARS); + aGarages[i].RemoveCarsBlockingDoorNotInside(); + break; + case GARAGE_HIDEOUT_TWO: + aGarages[i].StoreAndRemoveCarsForThisHideout(aCarsInSafeHouse2, NUM_GARAGE_STORED_CARS); + aGarages[i].RemoveCarsBlockingDoorNotInside(); + break; + case GARAGE_HIDEOUT_THREE: + aGarages[i].StoreAndRemoveCarsForThisHideout(aCarsInSafeHouse3, NUM_GARAGE_STORED_CARS); + aGarages[i].RemoveCarsBlockingDoorNotInside(); + break; + default: + break; + } + } + aGarages[i].m_fDoorPos = 0.0f; + aGarages[i].UpdateDoorsHeight(); + } +} + +int32 CGarages::CountCarsInHideoutGarage(eGarageType type) +{ + int32 total = 0; + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) { + switch (type) { + case GARAGE_HIDEOUT_ONE: + total += (aCarsInSafeHouse1[i].HasCar()); + break; + case GARAGE_HIDEOUT_TWO: + total += (aCarsInSafeHouse2[i].HasCar()); + break; + case GARAGE_HIDEOUT_THREE: + total += (aCarsInSafeHouse3[i].HasCar()); + break; + } + } + return total; +} + +int32 CGarages::FindMaxNumStoredCarsForGarage(eGarageType type) +{ + switch (type) { + case GARAGE_HIDEOUT_ONE: + return LIMIT_CARS_IN_INDUSTRIAL; + case GARAGE_HIDEOUT_TWO: + return LIMIT_CARS_IN_COMMERCIAL; + case GARAGE_HIDEOUT_THREE: + return LIMIT_CARS_IN_SUBURBAN; + } + return 0; +} + +bool CGarages::IsPointWithinHideOutGarage(CVector& point) +{ + for (int i = 0; i < NUM_GARAGES; i++) { + switch (aGarages[i].m_eGarageType) { + case GARAGE_HIDEOUT_ONE: + case GARAGE_HIDEOUT_TWO: + case GARAGE_HIDEOUT_THREE: + if (point.x > aGarages[i].m_fX1 && point.x < aGarages[i].m_fX2 && + point.y > aGarages[i].m_fY1 && point.y < aGarages[i].m_fY2 && + point.z > aGarages[i].m_fZ1 && point.z < aGarages[i].m_fZ2) + return true; + } + } + return false; +} + +bool CGarages::IsPointWithinAnyGarage(CVector& point) +{ + for (int i = 0; i < NUM_GARAGES; i++) { + switch (aGarages[i].m_eGarageType) { + case GARAGE_NONE: + continue; + default: + if (point.x > aGarages[i].m_fX1 && point.x < aGarages[i].m_fX2 && + point.y > aGarages[i].m_fY1 && point.y < aGarages[i].m_fY2 && + point.z > aGarages[i].m_fZ1 && point.z < aGarages[i].m_fZ2) + return true; + } + } + return false; +} + +void CGarages::SetAllDoorsBackToOriginalHeight() +{ + for (int i = 0; i < NUM_GARAGES; i++) { + switch (aGarages[i].m_eGarageType) { + case GARAGE_NONE: + continue; + default: + aGarages[i].RefreshDoorPointers(true); + if (aGarages[i].m_pDoor1) { + aGarages[i].m_pDoor1->GetPosition().z = aGarages[i].m_fDoor1Z; + if (aGarages[i].m_pDoor1->IsObject()) + ((CObject*)aGarages[i].m_pDoor1)->m_objectMatrix.GetPosition().z = aGarages[i].m_fDoor1Z; + if (aGarages[i].m_bRotatedDoor) + aGarages[i].BuildRotatedDoorMatrix(aGarages[i].m_pDoor1, 0.0f); + aGarages[i].m_pDoor1->GetMatrix().UpdateRW(); + aGarages[i].m_pDoor1->UpdateRwFrame(); + } + if (aGarages[i].m_pDoor2) { + aGarages[i].m_pDoor2->GetPosition().z = aGarages[i].m_fDoor2Z; + if (aGarages[i].m_pDoor2->IsObject()) + ((CObject*)aGarages[i].m_pDoor2)->m_objectMatrix.GetPosition().z = aGarages[i].m_fDoor2Z; + if (aGarages[i].m_bRotatedDoor) + aGarages[i].BuildRotatedDoorMatrix(aGarages[i].m_pDoor2, 0.0f); + aGarages[i].m_pDoor2->GetMatrix().UpdateRW(); + aGarages[i].m_pDoor2->UpdateRwFrame(); + } + } + } +} + +void CGarages::Save(uint8 * buf, uint32 * size) +{ +#ifdef FIX_GARAGE_SIZE + *size = (6 * sizeof(uint32) + TOTAL_COLLECTCARS_GARAGES * sizeof(*CarTypesCollected) + sizeof(uint32) + 3 * NUM_GARAGE_STORED_CARS * sizeof(CStoredCar) + NUM_GARAGES * sizeof(CGarage)); +#else + * size = 5484; +#endif + CloseHideOutGaragesBeforeSave(); + WriteSaveBuf(buf, NumGarages); + WriteSaveBuf(buf, (uint32)BombsAreFree); + WriteSaveBuf(buf, (uint32)RespraysAreFree); + WriteSaveBuf(buf, CarsCollected); + WriteSaveBuf(buf, BankVansCollected); + WriteSaveBuf(buf, PoliceCarsCollected); + for (int i = 0; i < TOTAL_COLLECTCARS_GARAGES; i++) + WriteSaveBuf(buf, CarTypesCollected[i]); + WriteSaveBuf(buf, LastTimeHelpMessage); + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) { + WriteSaveBuf(buf, aCarsInSafeHouse1[i]); + WriteSaveBuf(buf, aCarsInSafeHouse2[i]); + WriteSaveBuf(buf, aCarsInSafeHouse3[i]); + } + for (int i = 0; i < NUM_GARAGES; i++) + WriteSaveBuf(buf, aGarages[i]); +} CStoredCar::CStoredCar(const CStoredCar & other) { @@ -1850,7 +2318,42 @@ CStoredCar::CStoredCar(const CStoredCar & other) m_nCarBombType = other.m_nCarBombType; } -WRAPPER void CGarages::Load(uint8 * buf, uint32 size) { EAXJMP(0x428940); } +void CGarages::Load(uint8* buf, uint32 size) +{ +#ifdef FIX_GARAGE_SIZE + assert(size == (6 * sizeof(uint32) + TOTAL_COLLECTCARS_GARAGES * sizeof(*CarTypesCollected) + sizeof(uint32) + 3 * NUM_GARAGE_STORED_CARS * sizeof(CStoredCar) + NUM_GARAGES * sizeof(CGarage)); +#else + assert(size == 5484); +#endif + CloseHideOutGaragesBeforeSave(); + NumGarages = ReadSaveBuf(buf); + BombsAreFree = ReadSaveBuf(buf); + RespraysAreFree = ReadSaveBuf(buf); + CarsCollected = ReadSaveBuf(buf); + BankVansCollected = ReadSaveBuf(buf); + PoliceCarsCollected = ReadSaveBuf(buf); + for (int i = 0; i < TOTAL_COLLECTCARS_GARAGES; i++) + CarTypesCollected[i] = ReadSaveBuf(buf); + LastTimeHelpMessage = ReadSaveBuf(buf); + for (int i = 0; i < NUM_GARAGE_STORED_CARS; i++) { + aCarsInSafeHouse1[i] = ReadSaveBuf(buf); + aCarsInSafeHouse2[i] = ReadSaveBuf(buf); + aCarsInSafeHouse3[i] = ReadSaveBuf(buf); + } + for (int i = 0; i < NUM_GARAGES; i++) { + aGarages[i] = ReadSaveBuf(buf); + aGarages[i].m_pDoor1 = nil; + aGarages[i].m_pDoor2 = nil; + aGarages[i].m_pTarget = nil; + aGarages[i].field_96 = nil; + aGarages[i].m_bRecreateDoorOnNextRefresh = true; + aGarages[i].RefreshDoorPointers(true); + if (aGarages[i].m_eGarageType == GARAGE_CRUSHER) + aGarages[i].UpdateCrusherAngle(); + else + aGarages[i].UpdateDoorsHeight(); + } +} bool CGarages::IsModelIndexADoor(uint32 id) @@ -1892,9 +2395,8 @@ CGarages::IsModelIndexADoor(uint32 id) STARTPATCHES -InjectHook(0x421C60, CGarages::Init, PATCH_JUMP); -#ifndef PS2 -InjectHook(0x421E10, CGarages::Shutdown, PATCH_JUMP); -#endif -InjectHook(0x421E40, CGarages::Update, PATCH_JUMP); + InjectHook(0x426B20, CGarages::TriggerMessage, PATCH_JUMP); // CCrane::Update, CCrane::FindCarInSectorList + InjectHook(0x427AB0, CGarages::IsPointInAGarageCameraZone, PATCH_JUMP); // CCamera::CamControl + InjectHook(0x427BC0, CGarages::CameraShouldBeOutside, PATCH_JUMP); // CCamera::CamControl + InjectHook(0x428940, CGarages::Load, PATCH_JUMP); // GenericLoad ENDPATCHES \ No newline at end of file diff --git a/src/control/Garages.h b/src/control/Garages.h index ffe24e3a..5d1063ca 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -67,6 +67,8 @@ class CStoredCar int8 m_nCarBombType; public: void Init() { m_nModelIndex = 0; } + void Clear() { m_nModelIndex = 0; } + bool HasCar() { return m_nModelIndex != 0; } CStoredCar(const CStoredCar& other); void StoreCar(CVehicle*); CVehicle* RestoreCar(); @@ -81,7 +83,7 @@ class CGarage public: eGarageType m_eGarageType; eGarageState m_eGarageState; - char field_2; + bool field_2; bool m_bClosingWithoutTargetCar; bool m_bDeactivated; bool m_bResprayHappened; @@ -110,13 +112,10 @@ public: float m_fDoor1Z; float m_fDoor2Z; uint32 m_nTimeToStartAction; - char m_bCollectedCarsState; - char field_89; - char field_90; - char field_91; + uint8 m_bCollectedCarsState; CVehicle *m_pTarget; - int field_96; - CStoredCar m_sStoredCar; + void* field_96; + CStoredCar m_sStoredCar; // not needed void OpenThisGarage(); void CloseThisGarage(); @@ -126,7 +125,7 @@ public: void Update(); float GetGarageCenterX() { return (m_fX1 + m_fX2) / 2; } float GetGarageCenterY() { return (m_fY1 + m_fY2) / 2; } - bool IsClose() + bool IsFar() { #ifdef FIX_BUGS return Abs(TheCamera.GetPosition().x - GetGarageCenterX()) > SWITCH_GARAGE_DISTANCE_CLOSE || From 24e4ecf5bbf8c419c9e303e46db18f1323175458 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Tue, 31 Mar 2020 00:46:44 +0300 Subject: [PATCH 55/70] bug fixes, reorganisation --- src/control/Garages.cpp | 28 ++++++++--------- src/control/Garages.h | 69 +++++++++++++++++++++++------------------ src/control/Script.cpp | 8 ++--- 3 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index f7211272..c63818e1 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -489,7 +489,7 @@ void CGarage::Update() DMAudio.PlayFrontEndSound(SOUND_GARAGE_BOMB_ALREADY_SET, 1); break; } - if (!CGarages::BombsAreFree && CWorld::Players[CWorld::PlayerInFocus].m_nMoney >= BOMB_PRICE) { + if (!CGarages::BombsAreFree && CWorld::Players[CWorld::PlayerInFocus].m_nMoney < BOMB_PRICE) { CGarages::TriggerMessage("GA_4", -1, 4000, -1); // "Car bombs are $1000 each" m_eGarageState = GS_OPENEDCONTAINSCAR; DMAudio.PlayFrontEndSound(SOUND_GARAGE_NO_MONEY, 1); @@ -1973,23 +1973,23 @@ bool CGarages::IsPointInAGarageCameraZone(CVector point) for (int i = 0; i < NUM_GARAGES; i++) { switch (aGarages[i].m_eGarageType) { case GARAGE_NONE: - continue; + break; case GARAGE_COLLECTCARS_1: case GARAGE_COLLECTCARS_2: case GARAGE_COLLECTCARS_3: - if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_COLLECTCARS < point.x || - aGarages[i].m_fX2 - MARGIN_FOR_CAMERA_COLLECTCARS > point.x || - aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_COLLECTCARS < point.y || - aGarages[i].m_fY2 - MARGIN_FOR_CAMERA_COLLECTCARS > point.y) - continue; - return true; + if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_COLLECTCARS <= point.x && + aGarages[i].m_fX2 + MARGIN_FOR_CAMERA_COLLECTCARS >= point.x && + aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_COLLECTCARS <= point.y && + aGarages[i].m_fY2 + MARGIN_FOR_CAMERA_COLLECTCARS >= point.y) + return true; + break; default: - if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_DEFAULT < point.x || - aGarages[i].m_fX2 - MARGIN_FOR_CAMERA_DEFAULT > point.x || - aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_DEFAULT < point.y || - aGarages[i].m_fY2 - MARGIN_FOR_CAMERA_DEFAULT > point.y) - continue; - return true; + if (aGarages[i].m_fX1 - MARGIN_FOR_CAMERA_DEFAULT <= point.x && + aGarages[i].m_fX2 + MARGIN_FOR_CAMERA_DEFAULT >= point.x && + aGarages[i].m_fY1 - MARGIN_FOR_CAMERA_DEFAULT <= point.y && + aGarages[i].m_fY2 + MARGIN_FOR_CAMERA_DEFAULT >= point.y) + return true; + break; } } return false; diff --git a/src/control/Garages.h b/src/control/Garages.h index 5d1063ca..8b88359a 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -80,10 +80,9 @@ static_assert(sizeof(CStoredCar) == 0x28, "CStoredCar"); class CGarage { -public: eGarageType m_eGarageType; eGarageState m_eGarageState; - bool field_2; + bool field_2; // unused bool m_bClosingWithoutTargetCar; bool m_bDeactivated; bool m_bResprayHappened; @@ -114,7 +113,7 @@ public: uint32 m_nTimeToStartAction; uint8 m_bCollectedCarsState; CVehicle *m_pTarget; - void* field_96; + void* field_96; // unused CStoredCar m_sStoredCar; // not needed void OpenThisGarage(); @@ -166,13 +165,15 @@ public: void FindDoorsEntities(); void FindDoorsEntitiesSectorList(CPtrList&, bool); void PlayerArrestedOrDied(); + + friend class CGarages; + friend class cAudioManager; }; static_assert(sizeof(CGarage) == 140, "CGarage"); class CGarages { -public: static int32 &BankVansCollected; static bool &BombsAreFree; static bool &RespraysAreFree; @@ -195,50 +196,56 @@ public: static CStoredCar(&aCarsInSafeHouse3)[NUM_GARAGE_STORED_CARS]; static int32 &AudioEntity; static bool &bCamShouldBeOutisde; + public: static void Init(void); #ifndef PS2 static void Shutdown(void); #endif - static int16 AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z2, eGarageType type, int32 targetId); - - static bool IsModelIndexADoor(uint32 id); - static void TriggerMessage(const char *text, int16, uint16 time, int16); - static void PrintMessages(void); - static bool HasCarBeenCrushed(int32); - static bool IsPointWithinHideOutGarage(CVector&); - static bool IsPointWithinAnyGarage(CVector&); - static void PlayerArrestedOrDied(); - static void Update(void); - static void Load(uint8 *buf, uint32 size); - static void Save(uint8 *buf, uint32 *size); + + static int16 AddOne(float X1, float Y1, float Z1, float X2, float Y2, float Z2, eGarageType type, int32 targetId); + static void ChangeGarageType(int16, eGarageType, int32); + static void PrintMessages(void); + static void TriggerMessage(const char* text, int16, uint16 time, int16); static void SetTargetCarForMissonGarage(int16, CVehicle*); static bool HasCarBeenDroppedOffYet(int16); - static void ActivateGarage(int16); static void DeActivateGarage(int16); + static void ActivateGarage(int16); static int32 QueryCarsCollected(int16); - static bool HasThisCarBeenCollected(int16, uint8); - static void ChangeGarageType(int16, eGarageType, int32); - static bool HasResprayHappened(int16); - static void GivePlayerDetonator(); + static bool HasImportExportGarageCollectedThisCar(int16, int8); static bool IsGarageOpen(int16); static bool IsGarageClosed(int16); + static bool HasThisCarBeenCollected(int16, uint8); + static void OpenGarage(int16 garage) { aGarages[garage].OpenThisGarage(); } + static void CloseGarage(int16 garage) { aGarages[garage].CloseThisGarage(); } + static bool HasResprayHappened(int16); static void SetGarageDoorToRotate(int16); - static bool HasImportExportGarageCollectedThisCar(int16, int8); static void SetLeaveCameraForThisGarage(int16); static bool IsThisCarWithinGarageArea(int16, CEntity*); - static bool IsCarSprayable(CVehicle*); - static int32 FindMaxNumStoredCarsForGarage(eGarageType); - static int32 CountCarsInHideoutGarage(eGarageType); + static bool HasCarBeenCrushed(int32); static bool IsPointInAGarageCameraZone(CVector); - static bool CameraShouldBeOutside(); - static void CloseHideOutGaragesBeforeSave(); - static void SetAllDoorsBackToOriginalHeight(); - - static int GetBombTypeForGarageType(eGarageType type) { return type - GARAGE_BOMBSHOP1 + 1; } - static int GetCarsCollectedIndexForGarageType(eGarageType type) { return type - GARAGE_COLLECTCARS_1; } + static bool CameraShouldBeOutside(void); + static void GivePlayerDetonator(void); + static void PlayerArrestedOrDied(void); + static bool IsPointWithinHideOutGarage(CVector&); + static bool IsPointWithinAnyGarage(CVector&); + static void SetAllDoorsBackToOriginalHeight(void); + static void Save(uint8* buf, uint32* size); + static void Load(uint8* buf, uint32 size); + static bool IsModelIndexADoor(uint32 id); + static void SetFreeBombs(bool bValue) { BombsAreFree = bValue; } + static void SetFreeResprays(bool bValue) { RespraysAreFree = bValue; } private: + static bool IsCarSprayable(CVehicle*); static float FindDoorHeightForMI(int32); + static void CloseHideOutGaragesBeforeSave(void); + static int32 CountCarsInHideoutGarage(eGarageType); + static int32 FindMaxNumStoredCarsForGarage(eGarageType); + static int32 GetBombTypeForGarageType(eGarageType type) { return type - GARAGE_BOMBSHOP1 + 1; } + static int32 GetCarsCollectedIndexForGarageType(eGarageType type) { return type - GARAGE_COLLECTCARS_1; } + + friend class cAudioManager; + friend class CGarage; }; diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 2ea5ee24..8a79ba1d 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -4671,7 +4671,7 @@ int8 CRunningScript::ProcessCommands500To599(int32 command) return 0; case COMMAND_SET_FREE_BOMBS: CollectParameters(&m_nIp, 1); - CGarages::BombsAreFree = (ScriptParams[0] != 0); + CGarages::SetFreeBombs(ScriptParams[0] != 0); return 0; #ifdef GTA_PS2 case COMMAND_SET_POWERPOINT: @@ -6662,7 +6662,7 @@ int8 CRunningScript::ProcessCommands800To899(int32 command) } case COMMAND_SET_FREE_RESPRAYS: CollectParameters(&m_nIp, 1); - CGarages::RespraysAreFree = (ScriptParams[0] != 0); + CGarages::SetFreeResprays(ScriptParams[0] != 0); return 0; case COMMAND_SET_PLAYER_VISIBLE: { @@ -7110,13 +7110,13 @@ int8 CRunningScript::ProcessCommands800To899(int32 command) case COMMAND_OPEN_GARAGE: { CollectParameters(&m_nIp, 1); - CGarages::aGarages[ScriptParams[0]].OpenThisGarage(); + CGarages::OpenGarage(ScriptParams[0]); return 0; } case COMMAND_CLOSE_GARAGE: { CollectParameters(&m_nIp, 1); - CGarages::aGarages[ScriptParams[0]].CloseThisGarage(); + CGarages::CloseGarage(ScriptParams[1]); return 0; } case COMMAND_WARP_CHAR_FROM_CAR_TO_COORD: From fcf7b9907750d77a44063ca6a9d090cd7d883a85 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Tue, 31 Mar 2020 00:50:09 +0300 Subject: [PATCH 56/70] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 5d5180fc..403db674 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,6 @@ CCullZone CCullZones CExplosion CFallingGlassPane -CGarage -CGarages CGlass CMenuManager - WIP CMotionBlurStreaks From b3bfde0db0e820eefa4716715e7463500b59ff8c Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Tue, 31 Mar 2020 00:53:51 +0300 Subject: [PATCH 57/70] added forgotten function --- src/control/Garages.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index c63818e1..805a2f5d 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -1859,7 +1859,10 @@ void CGarages::SetLeaveCameraForThisGarage(int16 garage) aGarages[garage].m_bCameraFollowsPlayer = true; } -WRAPPER bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity * pCar) { EAXJMP(0x427570); } +bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity * pCar) +{ + aGarages[garage].IsEntityEntirelyInside3D(pCar, 0.0f); +} bool CGarages::HasCarBeenCrushed(int32 handle) { From 357d88a4a8e38a65298375a9a4b71255e4fce3a1 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Tue, 31 Mar 2020 01:00:06 +0300 Subject: [PATCH 58/70] fix --- src/control/Garages.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 805a2f5d..aabad1a6 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -1861,7 +1861,7 @@ void CGarages::SetLeaveCameraForThisGarage(int16 garage) bool CGarages::IsThisCarWithinGarageArea(int16 garage, CEntity * pCar) { - aGarages[garage].IsEntityEntirelyInside3D(pCar, 0.0f); + return aGarages[garage].IsEntityEntirelyInside3D(pCar, 0.0f); } bool CGarages::HasCarBeenCrushed(int32 handle) From 5e2fe749bd7620522168c9cd3dc469f70ac49e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?eray=20or=C3=A7unus?= Date: Tue, 31 Mar 2020 05:54:19 +0300 Subject: [PATCH 59/70] Mouse free cam for peds&cars (under FREE_CAM) --- src/core/Cam.cpp | 673 +++++++++++++++++++++++++++++++++++- src/core/Camera.h | 1 + src/core/Frontend.cpp | 7 +- src/core/Frontend.h | 1 - src/core/re3.cpp | 6 +- src/peds/Ped.cpp | 12 + src/peds/PlayerPed.cpp | 89 ++++- src/vehicles/Automobile.cpp | 18 +- 8 files changed, 788 insertions(+), 19 deletions(-) diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index 5844b61a..dd1b5ce2 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -23,12 +23,18 @@ #include "SceneEdit.h" #include "Debug.h" #include "Camera.h" +#include "DMAudio.h" const float DefaultFOV = 70.0f; // beta: 80.0f bool PrintDebugCode = false; int16 &DebugCamMode = *(int16*)0x95CCF2; -bool bFreeCam = false; + +#ifdef FREE_CAM +bool bFreePadCam = false; +bool bFreeMouseCam = false; +int nPreviousMode = -1; +#endif void CCam::Init(void) @@ -140,7 +146,7 @@ CCam::Process(void) Process_FollowPedWithMouse(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #ifdef FREE_CAM - if(bFreeCam) + if(bFreePadCam) Process_FollowPed_Rotation(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #endif @@ -180,7 +186,12 @@ CCam::Process(void) Process_FlyBy(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; case MODE_CAM_ON_A_STRING: - Process_Cam_On_A_String(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); +#ifdef FREE_CAM + if(bFreeMouseCam || bFreePadCam) + Process_FollowCar_SA(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + else +#endif + Process_Cam_On_A_String(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; case MODE_REACTION: Process_ReactionCam(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); @@ -192,7 +203,12 @@ CCam::Process(void) Process_Chris_With_Binding_PlusRotation(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; case MODE_BEHINDBOAT: - Process_BehindBoat(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); +#ifdef FREE_CAM + if (bFreeMouseCam || bFreePadCam) + Process_FollowCar_SA(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); + else +#endif + Process_BehindBoat(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); break; case MODE_PLAYER_FALLEN_WATER: Process_Player_Fallen_Water(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); @@ -244,6 +260,9 @@ CCam::Process(void) Up = CVector(0.0f, 0.0f, 1.0f); } +#ifdef FREE_CAM + nPreviousMode = Mode; +#endif CVector TargetToCam = Source - m_cvecTargetCoorsForFudgeInter; float DistOnGround = TargetToCam.Magnitude2D(); m_fTrueBeta = CGeneral::GetATanOfXY(TargetToCam.x, TargetToCam.y); @@ -1530,7 +1549,12 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient TargetCoors.z += fTranslateCamUp; TargetCoors = DoAverageOnVector(TargetCoors); + // SA code +#ifdef FREE_CAM + if((bFreeMouseCam && Alpha > 0.0f) || (!bFreeMouseCam && Alpha > fBaseDist)) +#else if(Alpha > fBaseDist) // comparing an angle against a distance? +#endif CamDist = fBaseDist + Cos(min(Alpha*fFalloff, HALFPI))*fAngleDist; else CamDist = fBaseDist + Cos(Alpha)*fAngleDist; @@ -4441,7 +4465,7 @@ CCam::Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrient float MouseX = CPad::GetPad(0)->GetMouseX(); float MouseY = CPad::GetPad(0)->GetMouseY(); float LookLeftRight, LookUpDown; - if((MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ + if(bFreeMouseCam && (MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ UseMouse = true; LookLeftRight = -2.5f*MouseX; LookUpDown = 4.0f*MouseY; @@ -4572,6 +4596,645 @@ CCam::Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrient GetVectorsReadyForRW(); } + +// LCS cam hehe +void +CCam::Process_FollowCar_SA(const CVector& CameraTarget, float TargetOrientation, float, float) +{ + // Missing things on III CCam + static CVector m_aTargetHistoryPosOne; + static CVector m_aTargetHistoryPosTwo; + static CVector m_aTargetHistoryPosThree; + static int m_nCurrentHistoryPoints = 0; + static float lastBeta = -9999.0f; + static float lastAlpha = -9999.0f; + static float stepsLeftToChangeBetaByMouse; + static float dontCollideWithCars; + static bool alphaCorrected; + static float heightIncreaseMult; + + if (!CamTargetEntity->IsVehicle()) + return; + + CVehicle* car = (CVehicle*)CamTargetEntity; + CVector TargetCoors = CameraTarget; + uint8 camSetArrPos = 0; + + // We may need those later + bool isPlane = car->m_modelIndex == MI_DODO; + bool isHeli = false; + bool isBike = false; + bool isCar = car->IsCar() && !isPlane && !isHeli && !isBike; + + CPad* pad = CPad::GetPad(0); + + // Next direction is non-existent in III + uint8 nextDirectionIsForward = !(pad->GetLookBehindForCar() || pad->GetLookBehindForPed() || pad->GetLookLeft() || pad->GetLookRight()) && + DirectionWasLooking == LOOKING_FORWARD; + + if (car->m_modelIndex == MI_FIRETRUCK) { + camSetArrPos = 7; + } else if (car->m_modelIndex == MI_RCBANDIT) { + camSetArrPos = 5; + } else if (car->IsBoat()) { + camSetArrPos = 4; + } else if (isBike) { + camSetArrPos = 1; + } else if (isPlane) { + camSetArrPos = 3; + } else if (isHeli) { + camSetArrPos = 2; + } + + // LCS one but index 1(firetruck) moved to last + float CARCAM_SET[][15] = { + {1.3f, 1.0f, 0.4f, 10.0f, 15.0f, 0.5f, 1.0f, 1.0f, 0.85f, 0.2f, 0.075f, 0.05f, 0.8f, DEGTORAD(45.0f), DEGTORAD(89.0f)}, // cars + {1.1f, 1.0f, 0.1f, 10.0f, 11.0f, 0.5f, 1.0f, 1.0f, 0.85f, 0.2f, 0.075f, 0.05f, 0.75f, DEGTORAD(45.0f), DEGTORAD(89.0f)}, // bike + {1.1f, 1.0f, 0.2f, 10.0f, 15.0f, 0.05f, 0.05f, 0.0f, 0.9f, 0.05f, 0.01f, 0.05f, 1.0f, DEGTORAD(10.0f), DEGTORAD(70.0f)}, // heli (SA values) + {1.1f, 3.5f, 0.2f, 10.0f, 25.0f, 0.5f, 1.0f, 1.0f, 0.75f, 0.1f, 0.005f, 0.2f, 1.0f, DEGTORAD(89.0f), DEGTORAD(89.0f)}, // plane (SA values) + {0.9f, 1.0f, 0.1f, 10.0f, 15.0f, 0.5f, 1.0f, 0.0f, 0.9f, 0.05f, 0.005f, 0.05f, 1.0f, -0.2f, DEGTORAD(70.0f)}, // boat + {1.1f, 1.0f, 0.2f, 10.0f, 5.0f, 0.5f, 1.0f, 1.0f, 0.75f, 0.1f, 0.005f, 0.2f, 1.0f, DEGTORAD(45.0f), DEGTORAD(89.0f)}, // rc cars + {1.1f, 1.0f, 0.2f, 10.0f, 5.0f, 0.5f, 1.0f, 1.0f, 0.75f, 0.1f, 0.005f, 0.2f, 1.0f, DEGTORAD(20.0f), DEGTORAD(70.0f)}, // rc heli/planes + {1.3f, 1.0f, 0.4f, 10.0f, 15.0f, 0.5f, 1.0f, 1.0f, 0.85f, 0.2f, 0.075f, 0.05f, 0.8f, -0.18f, DEGTORAD(40.0f)}, // firetruck... + }; + + // RC Heli/planes use same alpha values with heli/planes (LCS firetruck will fallback to 0) + uint8 alphaArrPos = (camSetArrPos > 4 ? (isPlane ? 3 : (isHeli ? 2 : 0)) : camSetArrPos); + float zoomModeAlphaOffset = 0.0f; + static float ZmOneAlphaOffsetLCS[] = { 0.12f, 0.08f, 0.15f, 0.08f, 0.08f }; + static float ZmTwoAlphaOffsetLCS[] = { 0.1f, 0.08f, 0.3f, 0.08f, 0.08f }; + static float ZmThreeAlphaOffsetLCS[] = { 0.065f, 0.05f, 0.15f, 0.06f, 0.08f }; + + if (isHeli && car->m_status == STATUS_PLAYER_REMOTE) + zoomModeAlphaOffset = ZmTwoAlphaOffsetLCS[alphaArrPos]; + else { + switch ((int)TheCamera.CarZoomIndicator) { + // near + case 1: + zoomModeAlphaOffset = ZmOneAlphaOffsetLCS[alphaArrPos]; + break; + // mid + case 2: + zoomModeAlphaOffset = ZmTwoAlphaOffsetLCS[alphaArrPos]; + break; + // far + case 3: + zoomModeAlphaOffset = ZmThreeAlphaOffsetLCS[alphaArrPos]; + break; + default: + break; + } + } + + CColModel* carCol = (CColModel*)car->GetColModel(); + float colMaxZ = carCol->boundingBox.max.z; // As opposed to LCS and SA, VC does this: carCol->boundingBox.max.z - carCol->boundingBox.min.z; + float approxCarLength = 2.0f * Abs(carCol->boundingBox.min.y); // SA taxi min.y = -2.95, max.z = 0.883502f + + float newDistance = TheCamera.CarZoomValueSmooth + CARCAM_SET[camSetArrPos][1] + approxCarLength; + + float minDistForThisCar = approxCarLength * CARCAM_SET[camSetArrPos][3]; + + if (!isHeli || car->m_status == STATUS_PLAYER_REMOTE) { + float radiusToStayOutside = colMaxZ * CARCAM_SET[camSetArrPos][0] - CARCAM_SET[camSetArrPos][2]; + if (radiusToStayOutside > 0.0f) { + TargetCoors.z += radiusToStayOutside; + newDistance += radiusToStayOutside; + zoomModeAlphaOffset += 0.3f / newDistance * radiusToStayOutside; + } + } else { + // 0.6f = fTestShiftHeliCamTarget + TargetCoors.x += 0.6f * car->GetUp().x * colMaxZ; + TargetCoors.y += 0.6f * car->GetUp().y * colMaxZ; + TargetCoors.z += 0.6f * car->GetUp().z * colMaxZ; + } + + float minDistForVehType = CARCAM_SET[camSetArrPos][4]; + + if ((int)TheCamera.CarZoomIndicator == 1 && (camSetArrPos < 2 || camSetArrPos == 7)) { + minDistForVehType = minDistForVehType * 0.65f; + } + + float nextDistance = max(newDistance, minDistForVehType); + + CA_MAX_DISTANCE = newDistance; + CA_MIN_DISTANCE = 3.5f; + + if (ResetStatics) { + FOV = DefaultFOV; + + // GTA 3 has this in veh. camera + if (TheCamera.m_bIdleOn) + TheCamera.m_uiTimeWeEnteredIdle = CTimer::GetTimeInMilliseconds(); + } else { + if (isCar || isBike) { + // 0.4f: CAR_FOV_START_SPEED + if (DotProduct(car->GetForward(), car->m_vecMoveSpeed) > 0.4f) + FOV += (DotProduct(car->GetForward(), car->m_vecMoveSpeed) - 0.4f) * CTimer::GetTimeStep(); + } + + if (FOV > DefaultFOV) + // 0.98f: CAR_FOV_FADE_MULT + FOV = pow(0.98f, CTimer::GetTimeStep()) * (FOV - DefaultFOV) + DefaultFOV; + + if (FOV <= DefaultFOV + 30.0f) { + if (FOV < DefaultFOV) + FOV = DefaultFOV; + } else + FOV = DefaultFOV + 30.0f; + } + + // WORKAROUND: I still don't know how looking behind works (m_bCamDirectlyInFront is unused in III, they seem to use m_bUseTransitionBeta) + if (pad->GetLookBehindForCar()) + if (DirectionWasLooking == LOOKING_FORWARD || !LookingBehind) + TheCamera.m_bCamDirectlyInFront = true; + + // Taken from RotCamIfInFrontCar, because we don't call it anymore + if (!(pad->GetLookBehindForCar() || pad->GetLookBehindForPed() || pad->GetLookLeft() || pad->GetLookRight())) + if (DirectionWasLooking != LOOKING_FORWARD) + TheCamera.m_bCamDirectlyBehind = true; + + // Called when we just entered the car, just started to look behind or returned back from looking left, right or behind + if (ResetStatics || TheCamera.m_bCamDirectlyBehind || TheCamera.m_bCamDirectlyInFront) { + ResetStatics = false; + Rotating = false; + m_bCollisionChecksOn = true; + // TheCamera.m_bResetOldMatrix = 1; + + // Garage exit cam is not working well in III... + // if (!TheCamera.m_bJustCameOutOfGarage) // && !sthForScript) + // { + Alpha = 0.0f; + Beta = car->GetForward().Heading() - HALFPI; + if (TheCamera.m_bCamDirectlyInFront) { + Beta += PI; + } + // } + + BetaSpeed = 0.0; + AlphaSpeed = 0.0; + Distance = 1000.0; + + Front.x = -(cos(Beta) * cos(Alpha)); + Front.y = -(sin(Beta) * cos(Alpha)); + Front.z = sin(Alpha); + + m_aTargetHistoryPosOne = TargetCoors - nextDistance * Front; + + m_aTargetHistoryPosTwo = TargetCoors - newDistance * Front; + + m_nCurrentHistoryPoints = 0; + if (!TheCamera.m_bJustCameOutOfGarage) // && !sthForScript) + Alpha = -zoomModeAlphaOffset; + } + + Front = TargetCoors - m_aTargetHistoryPosOne; + Front.Normalise(); + + // Code that makes cam rotate around the car + float camRightHeading = Front.Heading() - HALFPI; + if (camRightHeading < -PI) + camRightHeading = camRightHeading + TWOPI; + + float velocityRightHeading; + if (car->m_vecMoveSpeed.Magnitude2D() <= 0.02f) + velocityRightHeading = camRightHeading; + else + velocityRightHeading = car->m_vecMoveSpeed.Heading() - HALFPI; + + if (velocityRightHeading < camRightHeading - PI) + velocityRightHeading = velocityRightHeading + TWOPI; + else if (velocityRightHeading > camRightHeading + PI) + velocityRightHeading = velocityRightHeading - TWOPI; + + float betaChangeMult1 = CTimer::GetTimeStep() * CARCAM_SET[camSetArrPos][10]; + float betaChangeLimit = CTimer::GetTimeStep() * CARCAM_SET[camSetArrPos][11]; + + float betaChangeMult2 = (car->m_vecMoveSpeed - DotProduct(car->m_vecMoveSpeed, Front) * Front).Magnitude(); + + float betaChange = min(1.0f, betaChangeMult1 * betaChangeMult2) * (velocityRightHeading - camRightHeading); + if (betaChange <= betaChangeLimit) { + if (betaChange < -betaChangeLimit) + betaChange = -betaChangeLimit; + } else { + betaChange = betaChangeLimit; + } + float targetBeta = camRightHeading + betaChange; + + if (targetBeta < Beta - HALFPI) + targetBeta += TWOPI; + else if (targetBeta > Beta + PI) + targetBeta -= TWOPI; + + float carPosChange = (TargetCoors - m_aTargetHistoryPosTwo).Magnitude(); + if (carPosChange < newDistance && newDistance > minDistForThisCar) { + newDistance = max(minDistForThisCar, carPosChange); + } + float maxAlphaAllowed = CARCAM_SET[camSetArrPos][13]; + + // Originally this is to prevent camera enter into car while we're stopping, but what about moving??? + // This is also original LCS and SA bug, or some attempt to fix lag. We'll never know + + // if (car->m_vecMoveSpeed.MagnitudeSqr() < sq(0.2f)) + if (car->m_modelIndex != MI_FIRETRUCK) { + // if (!isBike || GetMysteriousWheelRelatedThingBike(car) > 3) + // if (!isHeli && (!isPlane || car->GetWheelsOnGround())) { + + CVector left = CrossProduct(car->GetForward(), CVector(0.0f, 0.0f, 1.0f)); + left.Normalise(); + CVector up = CrossProduct(left, car->GetForward()); + up.Normalise(); + float lookingUp = DotProduct(up, Front); + if (lookingUp > 0.0f) { + float v88 = Asin(Abs(Sin(Beta - (car->GetForward().Heading() - HALFPI)))); + float v200; + if (v88 <= Atan2(carCol->boundingBox.max.x, -carCol->boundingBox.min.y)) { + v200 = (1.5f - carCol->boundingBox.min.y) / Cos(v88); + } else { + float a6g = 1.2f + carCol->boundingBox.max.x; + v200 = a6g / Cos(max(0.0f, HALFPI - v88)); + } + maxAlphaAllowed = Cos(Beta - (car->GetForward().Heading() - HALFPI)) * Atan2(car->GetForward().z, car->GetForward().Magnitude2D()) + + Atan2(TargetCoors.z - car->GetPosition().z + car->GetHeightAboveRoad(), v200 * 1.2f); + + if (isCar && ((CAutomobile*)car)->m_nWheelsOnGround > 1 && Abs(DotProduct(car->m_vecTurnSpeed, car->GetForward())) < 0.05f) { + maxAlphaAllowed += Cos(Beta - (car->GetForward().Heading() - HALFPI) + HALFPI) * Atan2(car->GetRight().z, car->GetRight().Magnitude2D()); + } + } + } + + float targetAlpha = Asin(clamp(Front.z, -1.0f, 1.0f)) - zoomModeAlphaOffset; + if (targetAlpha <= maxAlphaAllowed) { + if (targetAlpha < -CARCAM_SET[camSetArrPos][14]) + targetAlpha = -CARCAM_SET[camSetArrPos][14]; + } else { + targetAlpha = maxAlphaAllowed; + } + float maxAlphaBlendAmount = CTimer::GetTimeStep() * CARCAM_SET[camSetArrPos][6]; + float targetAlphaBlendAmount = (1.0f - pow(CARCAM_SET[camSetArrPos][5], CTimer::GetTimeStep())) * (targetAlpha - Alpha); + if (targetAlphaBlendAmount <= maxAlphaBlendAmount) { + if (targetAlphaBlendAmount < -maxAlphaBlendAmount) + targetAlphaBlendAmount = -maxAlphaBlendAmount; + } else { + targetAlphaBlendAmount = maxAlphaBlendAmount; + } + + // Using GetCarGun(LR/UD) will give us same unprocessed RightStick value as SA + float stickX = -(pad->GetCarGunLeftRight()); + float stickY = pad->GetCarGunUpDown(); + + // In SA this checks for m_bUseMouse3rdPerson so num2/num8 do not move camera when Keyboard & Mouse controls are used. + if (CCamera::m_bUseMouse3rdPerson) + stickY = 0.0f; + + float xMovement = Abs(stickX) * (FOV / 80.0f * 5.f / 70.f) * stickX * 0.007f * 0.007f; + float yMovement = Abs(stickY) * (FOV / 80.0f * 3.f / 70.f) * stickY * 0.007f * 0.007f; + + bool correctAlpha = true; + // if (SA checks if we aren't in work car, why?) { + if (!isCar || car->m_modelIndex != MI_YARDIE) { + correctAlpha = false; + } + else { + xMovement = 0.0f; + yMovement = 0.0f; + } + // } else + // yMovement = 0.0; + + if (!nextDirectionIsForward) { + yMovement = 0.0; + xMovement = 0.0; + } + + if (camSetArrPos == 0 || camSetArrPos == 7) { + // This is not working on cars as SA + // Because III/VC doesn't have any buttons tied to LeftStick if you're not in Classic Configuration, using Dodo or using GInput/Pad, so :shrug: + if (Abs(pad->GetSteeringUpDown()) > 120.0f) { + if (car->pDriver && car->pDriver->m_objective != OBJECTIVE_LEAVE_VEHICLE) { + yMovement += Abs(pad->GetSteeringUpDown()) * (FOV / 80.0f * 3.f / 70.f) * pad->GetSteeringUpDown() * 0.007f * 0.007f * 0.5; + } + } + } + + if (yMovement > 0.0) + yMovement = yMovement * 0.5; + + bool mouseChangesBeta = false; + + // FIX: Disable mouse movement in drive-by, it's buggy. Original SA bug. + if (bFreeMouseCam && CCamera::m_bUseMouse3rdPerson && !pad->ArePlayerControlsDisabled() && nextDirectionIsForward) { + float mouseY = pad->GetMouseY() * 2.0f; + float mouseX = pad->GetMouseX() * -2.0f; + + // If you want an ability to toggle free cam while steering with mouse, you can add an OR after DisableMouseSteering. + // There was a pad->NewState.m_bVehicleMouseLook in SA, which doesn't exists in III. + + if ((mouseX != 0.0 || mouseY != 0.0) && (CVehicle::m_bDisableMouseSteering)) { + yMovement = mouseY * FOV / 80.0f * TheCamera.m_fMouseAccelHorzntl; // Same as SA, horizontal sensitivity. + BetaSpeed = 0.0; + AlphaSpeed = 0.0; + xMovement = mouseX * FOV / 80.0f * TheCamera.m_fMouseAccelHorzntl; + targetAlpha = Alpha; + stepsLeftToChangeBetaByMouse = 1.0f * 50.0f; + mouseChangesBeta = true; + } else if (stepsLeftToChangeBetaByMouse > 0.0f) { + // Finish rotation by decreasing speed when we stopped moving mouse + BetaSpeed = 0.0; + AlphaSpeed = 0.0; + yMovement = 0.0; + xMovement = 0.0; + targetAlpha = Alpha; + stepsLeftToChangeBetaByMouse = max(0.0f, stepsLeftToChangeBetaByMouse - CTimer::GetTimeStep()); + mouseChangesBeta = true; + } + } + + if (correctAlpha) { + if (nPreviousMode != MODE_CAM_ON_A_STRING) + alphaCorrected = false; + + if (!alphaCorrected && Abs(zoomModeAlphaOffset + Alpha) > 0.05f) { + yMovement = (-zoomModeAlphaOffset - Alpha) * 0.05f; + } else + alphaCorrected = true; + } + float alphaSpeedFromStickY = yMovement * CARCAM_SET[camSetArrPos][12]; + float betaSpeedFromStickX = xMovement * CARCAM_SET[camSetArrPos][12]; + + float newAngleSpeedMaxBlendAmount = CARCAM_SET[camSetArrPos][9]; + float angleChangeStep = pow(CARCAM_SET[camSetArrPos][8], CTimer::GetTimeStep()); + float targetBetaWithStickBlendAmount = betaSpeedFromStickX + (targetBeta - Beta) / max(CTimer::GetTimeStep(), 1.0f); + + if (targetBetaWithStickBlendAmount < -newAngleSpeedMaxBlendAmount) + targetBetaWithStickBlendAmount = -newAngleSpeedMaxBlendAmount; + else if (targetBetaWithStickBlendAmount > newAngleSpeedMaxBlendAmount) + targetBetaWithStickBlendAmount = newAngleSpeedMaxBlendAmount; + + float angleChangeStepLeft = 1.0f - angleChangeStep; + BetaSpeed = targetBetaWithStickBlendAmount * angleChangeStepLeft + angleChangeStep * BetaSpeed; + if (Abs(BetaSpeed) < 0.0001f) + BetaSpeed = 0.0f; + + float betaChangePerFrame; + if (mouseChangesBeta) + betaChangePerFrame = betaSpeedFromStickX; + else + betaChangePerFrame = CTimer::GetTimeStep() * BetaSpeed; + Beta = betaChangePerFrame + Beta; + + if (TheCamera.m_bJustCameOutOfGarage) { + float invHeading = Atan2(Front.y, Front.x); + if (invHeading < 0.0f) + invHeading += TWOPI; + + Beta = invHeading + PI; + } + + Beta = CGeneral::LimitRadianAngle(Beta); + if (Beta < 0.0f) + Beta += TWOPI; + + if ((camSetArrPos <= 1 || camSetArrPos == 7) && targetAlpha < Alpha && carPosChange >= newDistance) { + if (isCar && ((CAutomobile*)car)->m_nWheelsOnGround > 1) + // || isBike && GetMysteriousWheelRelatedThingBike(car) > 1) + alphaSpeedFromStickY += (targetAlpha - Alpha) * 0.075f; + } + + AlphaSpeed = angleChangeStepLeft * alphaSpeedFromStickY + angleChangeStep * AlphaSpeed; + float maxAlphaSpeed = newAngleSpeedMaxBlendAmount; + if (alphaSpeedFromStickY > 0.0f) + maxAlphaSpeed = maxAlphaSpeed * 0.5; + + if (AlphaSpeed <= maxAlphaSpeed) { + float minAlphaSpeed = -maxAlphaSpeed; + if (AlphaSpeed < minAlphaSpeed) + AlphaSpeed = minAlphaSpeed; + } else { + AlphaSpeed = maxAlphaSpeed; + } + + if (Abs(AlphaSpeed) < 0.0001f) + AlphaSpeed = 0.0f; + + float alphaWithSpeedAccounted; + if (mouseChangesBeta) { + alphaWithSpeedAccounted = alphaSpeedFromStickY + targetAlpha; + Alpha += alphaSpeedFromStickY; + } else { + alphaWithSpeedAccounted = CTimer::GetTimeStep() * AlphaSpeed + targetAlpha; + Alpha += targetAlphaBlendAmount; + } + + if (Alpha <= maxAlphaAllowed) { + float minAlphaAllowed = -CARCAM_SET[camSetArrPos][14]; + if (minAlphaAllowed > Alpha) { + Alpha = minAlphaAllowed; + AlphaSpeed = 0.0f; + } + } else { + Alpha = maxAlphaAllowed; + AlphaSpeed = 0.0f; + } + + // Prevent unsignificant angle changes + if (Abs(lastAlpha - Alpha) < 0.0001f) + Alpha = lastAlpha; + + lastAlpha = Alpha; + + if (Abs(lastBeta - Beta) < 0.0001f) + Beta = lastBeta; + + lastBeta = Beta; + + Front.x = -(cos(Beta) * cos(Alpha)); + Front.y = -(sin(Beta) * cos(Alpha)); + Front.z = sin(Alpha); + GetVectorsReadyForRW(); + TheCamera.m_bCamDirectlyBehind = false; + TheCamera.m_bCamDirectlyInFront = false; + + Source = TargetCoors - newDistance * Front; + + m_cvecTargetCoorsForFudgeInter = TargetCoors; + m_aTargetHistoryPosThree = m_aTargetHistoryPosOne; + float nextAlpha = alphaWithSpeedAccounted + zoomModeAlphaOffset; + float nextFrontX = -(cos(Beta) * cos(nextAlpha)); + float nextFrontY = -(sin(Beta) * cos(nextAlpha)); + float nextFrontZ = sin(nextAlpha); + + m_aTargetHistoryPosOne.x = TargetCoors.x - nextFrontX * nextDistance; + m_aTargetHistoryPosOne.y = TargetCoors.y - nextFrontY * nextDistance; + m_aTargetHistoryPosOne.z = TargetCoors.z - nextFrontZ * nextDistance; + + m_aTargetHistoryPosTwo.x = TargetCoors.x - nextFrontX * newDistance; + m_aTargetHistoryPosTwo.y = TargetCoors.y - nextFrontY * newDistance; + m_aTargetHistoryPosTwo.z = TargetCoors.z - nextFrontZ * newDistance; + + // SA calls SetColVarsVehicle in here + if (nextDirectionIsForward) { + + // This is new in LCS! + float timestepFactor = Pow(0.99f, CTimer::GetTimeStep()); + dontCollideWithCars = (timestepFactor * dontCollideWithCars) + ((1.0f - timestepFactor) * car->m_vecMoveSpeed.Magnitude()); + + // Move cam if on collision + CColPoint foundCol; + CEntity* foundEnt; + CWorld::pIgnoreEntity = CamTargetEntity; + if (CWorld::ProcessLineOfSight(TargetCoors, Source, foundCol, foundEnt, true, dontCollideWithCars < 0.1f, false, true, false, true, false)) { + float obstacleTargetDist = (TargetCoors - foundCol.point).Magnitude(); + float obstacleCamDist = newDistance - obstacleTargetDist; + if (!foundEnt->IsPed() || obstacleCamDist <= 1.0f) { + Source = foundCol.point; + if (obstacleTargetDist < 1.2f) { + RwCameraSetNearClipPlane(Scene.camera, max(0.05f, obstacleTargetDist - 0.3f)); + } + } else { + if (!CWorld::ProcessLineOfSight(foundCol.point, Source, foundCol, foundEnt, true, dontCollideWithCars < 0.1f, false, true, false, true, false)) { + float lessClip = obstacleCamDist - 0.35f; + if (lessClip <= 0.9f) + RwCameraSetNearClipPlane(Scene.camera, lessClip); + else + RwCameraSetNearClipPlane(Scene.camera, 0.9f); + } else { + obstacleTargetDist = (TargetCoors - foundCol.point).Magnitude(); + Source = foundCol.point; + if (obstacleTargetDist < 1.2f) { + float lessClip = obstacleTargetDist - 0.3f; + if (lessClip >= 0.05f) + RwCameraSetNearClipPlane(Scene.camera, lessClip); + else + RwCameraSetNearClipPlane(Scene.camera, 0.05f); + } + } + } + } + CWorld::pIgnoreEntity = nil; + float nearClip = RwCameraGetNearClipPlane(Scene.camera); + float radius = Tan(DEGTORAD(FOV * 0.5f)) * CDraw::GetAspectRatio() * 1.1f; + + // If we're seeing blue hell due to camera intersects some surface, fix it. + // SA and LCS have this unrolled. + for (int i = 0; + i <= 5 && CWorld::TestSphereAgainstWorld((nearClip * Front) + Source, radius * nearClip, nil, true, true, false, true, false, false); + i++) { + + CVector surfaceCamDist = gaTempSphereColPoints->point - Source; + CVector frontButInvertedIfTouchesSurface = DotProduct(surfaceCamDist, Front) * Front; + float newNearClip = (surfaceCamDist - frontButInvertedIfTouchesSurface).Magnitude() / radius; + + if (newNearClip > nearClip) + newNearClip = nearClip; + if (newNearClip < 0.1f) + newNearClip = 0.1f; + if (nearClip > newNearClip) + RwCameraSetNearClipPlane(Scene.camera, newNearClip); + + if (newNearClip == 0.1f) + Source += (TargetCoors - Source) * 0.3f; + + nearClip = RwCameraGetNearClipPlane(Scene.camera); + radius = Tan(DEGTORAD(FOV * 0.5f)) * CDraw::GetAspectRatio() * 1.1f; + } + } + TheCamera.m_bCamDirectlyBehind = false; + TheCamera.m_bCamDirectlyInFront = false; + + // ------- LCS specific part starts + + if (camSetArrPos == 5 && Source.z < 1.0f) // RC Bandit and Baron + Source.z = 1.0f; + + // Obviously some specific place in LC + if (Source.x > 11.0f && Source.x < 91.0f) { + if (Source.y > -680.0f && Source.y < -600.0f && Source.z < 24.4f) + Source.z = 24.4f; + } + + // CCam::FixSourceAboveWaterLevel + if (CameraTarget.z >= -2.0f) { + float level = -6000.0; + // +0.5f is needed for III + if (CWaterLevel::GetWaterLevelNoWaves(Source.x, Source.y, Source.z, &level)) { + if (Source.z < level + 0.5f) + Source.z = level + 0.5f; + } + } + Front = TargetCoors - Source; + + // -------- LCS specific part ends + + GetVectorsReadyForRW(); + // SA + // gTargetCoordsForLookingBehind = TargetCoors; + + // SA code from CAutomobile::TankControl/FireTruckControl. + if (car->m_modelIndex == MI_RHINO || car->m_modelIndex == MI_FIRETRUCK) { + + float &carGunLR = ((CAutomobile*)car)->m_fCarGunLR; + CVector hi = Multiply3x3(Front, car->GetMatrix()); + + // III/VC's firetruck turret angle is reversed + float angleToFace = (car->m_modelIndex == MI_FIRETRUCK ? -hi.Heading() : hi.Heading()); + + if (angleToFace <= carGunLR + PI) { + if (angleToFace < carGunLR - PI) + angleToFace = angleToFace + TWOPI; + } else { + angleToFace = angleToFace - TWOPI; + } + + float neededTurn = angleToFace - carGunLR; + float turnPerFrame = CTimer::GetTimeStep() * (car->m_modelIndex == MI_FIRETRUCK ? 0.05f : 0.015f); + if (neededTurn <= turnPerFrame) { + if (neededTurn < -turnPerFrame) + angleToFace = carGunLR - turnPerFrame; + } else { + angleToFace = turnPerFrame + carGunLR; + } + + if (car->m_modelIndex == MI_RHINO && carGunLR != angleToFace) { + DMAudio.PlayOneShot(car->m_audioEntityId, SOUND_CAR_TANK_TURRET_ROTATE, Abs(angleToFace - carGunLR)); + } + carGunLR = angleToFace; + + if (carGunLR < -PI) { + carGunLR += TWOPI; + } else if (carGunLR > PI) { + carGunLR -= TWOPI; + } + + // Because firetruk turret also has Y movement + if (car->m_modelIndex == MI_FIRETRUCK) { + float &carGunUD = ((CAutomobile*)car)->m_fCarGunUD; + + float alphaToFace = Atan2(hi.z, hi.Magnitude2D()) + DEGTORAD(15.0f); + float neededAlphaTurn = alphaToFace - carGunUD; + float alphaTurnPerFrame = CTimer::GetTimeStep() * 0.02f; + + if (neededAlphaTurn > alphaTurnPerFrame) { + neededTurn = alphaTurnPerFrame; + carGunUD = neededTurn + carGunUD; + } else { + if (neededAlphaTurn >= -alphaTurnPerFrame) { + carGunUD = alphaToFace; + } else { + carGunUD = carGunUD - alphaTurnPerFrame; + } + } + + float turretMinY = -DEGTORAD(20.0f); + float turretMaxY = DEGTORAD(20.0f); + if (turretMinY <= carGunUD) { + if (carGunUD > turretMaxY) + carGunUD = turretMaxY; + } else { + carGunUD = turretMinY; + } + } + } +} #endif STARTPATCHES diff --git a/src/core/Camera.h b/src/core/Camera.h index 3dc74fe7..f3e3e661 100644 --- a/src/core/Camera.h +++ b/src/core/Camera.h @@ -224,6 +224,7 @@ struct CCam // custom stuff void Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrientation, float, float); + void Process_FollowCar_SA(const CVector &CameraTarget, float TargetOrientation, float, float); }; static_assert(sizeof(CCam) == 0x1A4, "CCam: wrong size"); static_assert(offsetof(CCam, Alpha) == 0xA8, "CCam: error"); diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index a469a215..50d50dbe 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -64,7 +64,6 @@ bool &CMenuManager::m_bShutDownFrontEndRequested = *(bool*)0x95CD6A; int8 &CMenuManager::m_PrefsUseWideScreen = *(int8*)0x95CD23; int8 &CMenuManager::m_PrefsRadioStation = *(int8*)0x95CDA4; -int8 &CMenuManager::m_bDisableMouseSteering = *(int8*)0x60252C; // 1 int32 &CMenuManager::m_PrefsBrightness = *(int32*)0x5F2E50; // 256 float &CMenuManager::m_PrefsLOD = *(float*)0x8F42C4; int8 &CMenuManager::m_bFrontEnd_ReloadObrTxtGxt = *(int8*)0x628CFC; @@ -968,7 +967,7 @@ void CMenuManager::Draw() rightText = TheText.Get(m_PrefsDMA ? "FEM_ON" : "FEM_OFF"); break; case MENUACTION_MOUSESTEER: - rightText = TheText.Get(m_bDisableMouseSteering ? "FEM_OFF" : "FEM_ON"); + rightText = TheText.Get(CVehicle::m_bDisableMouseSteering ? "FEM_OFF" : "FEM_ON"); break; } @@ -3141,7 +3140,7 @@ CMenuManager::ProcessButtonPresses(void) CMenuManager::m_ControlMethod = CONTROL_STANDART; MousePointerStateHelper.bInvertVertically = false; TheCamera.m_fMouseAccelHorzntl = 0.0025f; - m_bDisableMouseSteering = true; + CVehicle::m_bDisableMouseSteering = true; TheCamera.m_bHeadBob = false; SaveSettings(); } @@ -3460,7 +3459,7 @@ void CMenuManager::ProcessOnOffMenuOptions() SaveSettings(); break; case MENUACTION_MOUSESTEER: - m_bDisableMouseSteering = !m_bDisableMouseSteering; + CVehicle::m_bDisableMouseSteering = !CVehicle::m_bDisableMouseSteering; DMAudio.PlayFrontEndSound(SOUND_FRONTEND_MENU_SUCCESS, 0); SaveSettings(); break; diff --git a/src/core/Frontend.h b/src/core/Frontend.h index 3dbed164..53363f07 100644 --- a/src/core/Frontend.h +++ b/src/core/Frontend.h @@ -473,7 +473,6 @@ public: static int32 &m_ControlMethod; static int8 &m_PrefsDMA; static int32 &m_PrefsLanguage; - static int8 &m_bDisableMouseSteering; static int32 &m_PrefsBrightness; static float &m_PrefsLOD; static int8 &m_bFrontEnd_ReloadObrTxtGxt; diff --git a/src/core/re3.cpp b/src/core/re3.cpp index 500bf230..05d28167 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -373,8 +373,10 @@ DebugMenuPopulate(void) extern bool PrintDebugCode; extern int16 &DebugCamMode; #ifdef FREE_CAM - extern bool bFreeCam; - DebugMenuAddVarBool8("Cam", "Free Cam", (int8*)&bFreeCam, nil); + extern bool bFreePadCam; + extern bool bFreeMouseCam; + DebugMenuAddVarBool8("Cam", "Free Gamepad Cam", (int8*)&bFreePadCam, nil); + DebugMenuAddVarBool8("Cam", "Free Mouse Cam", (int8*)&bFreeMouseCam, nil); #endif DebugMenuAddVarBool8("Cam", "Print Debug Code", (int8*)&PrintDebugCode, nil); DebugMenuAddVar("Cam", "Cam Mode", &DebugCamMode, nil, 1, 0, CCam::MODE_EDITOR, nil); diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index f43feae5..264fa669 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -59,6 +59,10 @@ #define CAN_SEE_ENTITY_ANGLE_THRESHOLD DEGTORAD(60.0f) +#ifdef FREE_CAM +extern bool bFreeMouseCam; +#endif + CPed *gapTempPedList[50]; uint16 gnNumTempPedList; @@ -807,6 +811,10 @@ CPed::IsPedInControl(void) bool CPed::CanStrafeOrMouseControl(void) { +#ifdef FREE_CAM + if (bFreeMouseCam) + return false; +#endif return m_nPedState == PED_NONE || m_nPedState == PED_IDLE || m_nPedState == PED_FLEE_POS || m_nPedState == PED_FLEE_ENTITY || m_nPedState == PED_ATTACK || m_nPedState == PED_FIGHT || m_nPedState == PED_AIM_GUN || m_nPedState == PED_JUMP; } @@ -6984,7 +6992,11 @@ CPed::FinishLaunchCB(CAnimBlendAssociation *animAssoc, void *arg) #endif ) { +#ifdef FREE_CAM + if (TheCamera.Cams[0].Using3rdPersonMouseCam() && !bFreeMouseCam) { +#else if (TheCamera.Cams[0].Using3rdPersonMouseCam()) { +#endif float fpsAngle = ped->WorkOutHeadingForMovingFirstPerson(ped->m_fRotationCur); ped->m_vecMoveSpeed.x = -velocityFromAnim * Sin(fpsAngle); ped->m_vecMoveSpeed.y = velocityFromAnim * Cos(fpsAngle); diff --git a/src/peds/PlayerPed.cpp b/src/peds/PlayerPed.cpp index 5275f716..cd2cac23 100644 --- a/src/peds/PlayerPed.cpp +++ b/src/peds/PlayerPed.cpp @@ -18,6 +18,10 @@ #define PAD_MOVE_TO_GAME_WORLD_MOVE 60.0f +#ifdef FREE_CAM +extern bool bFreeMouseCam; +#endif + CPlayerPed::~CPlayerPed() { delete m_pWanted; @@ -65,7 +69,7 @@ void CPlayerPed::ClearWeaponTarget() TheCamera.ClearPlayerWeaponMode(); CWeaponEffects::ClearCrossHair(); } - ClearPointGunAt(); + ClearPointGunAt(); } void @@ -688,7 +692,14 @@ CPlayerPed::PlayerControl1stPersonRunAround(CPad *padUsed) float padMove = CVector2D(leftRight, upDown).Magnitude(); float padMoveInGameUnit = padMove / PAD_MOVE_TO_GAME_WORLD_MOVE; if (padMoveInGameUnit > 0.0f) { +#ifdef FREE_CAM + if (!bFreeMouseCam) + m_fRotationDest = CGeneral::LimitRadianAngle(TheCamera.Orientation); + else + m_fRotationDest = CGeneral::GetRadianAngleBetweenPoints(0.0f, 0.0f, -leftRight, upDown) - TheCamera.Orientation; +#else m_fRotationDest = CGeneral::LimitRadianAngle(TheCamera.Orientation); +#endif m_fMoveSpeed = min(padMoveInGameUnit, 0.07f * CTimer::GetTimeStep() + m_fMoveSpeed); } else { m_fMoveSpeed = 0.0f; @@ -981,6 +992,12 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) if (padUsed->TargetJustDown()) { SetStoredState(); m_nPedState = PED_SNIPER_MODE; +#ifdef FREE_CAM + if (bFreeMouseCam && TheCamera.Cams[0].Using3rdPersonMouseCam()) { + m_fRotationCur = CGeneral::LimitRadianAngle(-TheCamera.Orientation); + SetHeading(m_fRotationCur); + } +#endif if (GetWeapon()->m_eWeaponType == WEAPONTYPE_ROCKETLAUNCHER) TheCamera.SetNewPlayerWeaponMode(CCam::MODE_ROCKETLAUNCHER, 0, 0); else if (GetWeapon()->m_eWeaponType == WEAPONTYPE_SNIPERRIFLE) @@ -1000,7 +1017,12 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) if (padUsed->GetWeapon() && m_nMoveState != PEDMOVE_SPRINT) { if (m_nSelectedWepSlot == m_currentWeapon) { if (m_pPointGunAt) { - SetAttack(m_pPointGunAt); +#ifdef FREE_CAM + if (bFreeMouseCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE && m_fMoveSpeed < 1.0f) + StartFightAttack(padUsed->GetWeapon()); + else +#endif + SetAttack(m_pPointGunAt); } else if (m_currentWeapon != WEAPONTYPE_UNARMED) { if (m_nPedState == PED_ATTACK) { if (padUsed->WeaponJustDown()) { @@ -1027,11 +1049,65 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) bIsAttacking = false; } } + +#ifdef FREE_CAM + // Rotate player/arm when shooting. We don't have auto-rotation anymore + if (CCamera::m_bUseMouse3rdPerson && bFreeMouseCam && + m_nSelectedWepSlot == m_currentWeapon && m_nMoveState != PEDMOVE_SPRINT) { + + // Weapons except throwable and melee ones + if (weaponInfo->m_bCanAim || weaponInfo->m_b1stPerson || weaponInfo->m_bExpands) { + if ((padUsed->GetTarget() && weaponInfo->m_bCanAimWithArm) || padUsed->GetWeapon()) { + float limitedCam = CGeneral::LimitRadianAngle(-TheCamera.Orientation); + + // On this one we can rotate arm. + if (weaponInfo->m_bCanAimWithArm) { + if (!padUsed->GetWeapon()) { // making this State != ATTACK still stops it after attack. Re-start it immediately! + SetPointGunAt(nil); + bIsPointingGunAt = false; // to not stop after attack + } + + SetLookFlag(limitedCam, true); + SetAimFlag(limitedCam); +#ifdef VC_PED_PORTS + SetLookTimer(INT_MAX); // removing this makes head move for real, but I experinced some bugs. +#endif + } else { + m_fRotationDest = limitedCam; + m_headingRate = 50.0f; + + // Anim. fix for shotgun, ak47 and m16 (we must finish rot. it quickly) + if (weaponInfo->m_bCanAim && padUsed->WeaponJustDown()) { + m_fRotationCur = CGeneral::LimitRadianAngle(m_fRotationCur); + float limitedRotDest = m_fRotationDest; + + if (m_fRotationCur - PI > m_fRotationDest) { + limitedRotDest += 2 * PI; + } else if (PI + m_fRotationCur < m_fRotationDest) { + limitedRotDest -= 2 * PI; + } + + m_fRotationCur += (limitedRotDest - m_fRotationCur) / 2; + } + } + } else if (weaponInfo->m_bCanAimWithArm) + ClearPointGunAt(); + else + RestoreHeadingRate(); + } + } +#endif + if (padUsed->GetTarget() && m_nSelectedWepSlot == m_currentWeapon && m_nMoveState != PEDMOVE_SPRINT) { if (m_pPointGunAt) { // what?? if (!m_pPointGunAt - || CCamera::m_bUseMouse3rdPerson || m_pPointGunAt->IsPed() && ((CPed*)m_pPointGunAt)->bInVehicle) { +#ifdef FREE_CAM + || (!bFreeMouseCam && CCamera::m_bUseMouse3rdPerson) +#else + || CCamera::m_bUseMouse3rdPerson +#endif + || m_pPointGunAt->IsPed() && ((CPed*)m_pPointGunAt)->bInVehicle) { ClearWeaponTarget(); return; } @@ -1047,7 +1123,12 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) } TheCamera.SetNewPlayerWeaponMode(CCam::MODE_SYPHON, 0, 0); TheCamera.UpdateAimingCoors(m_pPointGunAt->GetPosition()); - } else if (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson) { + } +#ifdef FREE_CAM + else if ((bFreeMouseCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE) || (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson)) { +#else + else if (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson) { +#endif if (padUsed->TargetJustDown()) FindWeaponLockOnTarget(); } diff --git a/src/vehicles/Automobile.cpp b/src/vehicles/Automobile.cpp index e709a87f..aca96aa3 100644 --- a/src/vehicles/Automobile.cpp +++ b/src/vehicles/Automobile.cpp @@ -2339,10 +2339,17 @@ CAutomobile::FireTruckControl(void) if(this == FindPlayerVehicle()){ if(!CPad::GetPad(0)->GetWeapon()) return; - m_fCarGunLR += CPad::GetPad(0)->GetCarGunLeftRight()*0.00025f*CTimer::GetTimeStep(); - m_fCarGunUD += CPad::GetPad(0)->GetCarGunUpDown()*0.0001f*CTimer::GetTimeStep(); +#ifdef FREE_CAM + extern bool bFreeMouseCam; + if (!bFreeMouseCam) +#endif + { + m_fCarGunLR += CPad::GetPad(0)->GetCarGunLeftRight() * 0.00025f * CTimer::GetTimeStep(); + m_fCarGunUD += CPad::GetPad(0)->GetCarGunUpDown() * 0.0001f * CTimer::GetTimeStep(); + } m_fCarGunUD = clamp(m_fCarGunUD, 0.05f, 0.3f); + CVector cannonPos(0.0f, 1.5f, 1.9f); cannonPos = GetMatrix() * cannonPos; CVector cannonDir( @@ -2408,7 +2415,12 @@ CAutomobile::TankControl(void) // Rotate turret float prevAngle = m_fCarGunLR; - m_fCarGunLR -= CPad::GetPad(0)->GetCarGunLeftRight() * 0.00015f * CTimer::GetTimeStep(); +#ifdef FREE_CAM + extern bool bFreeMouseCam; + if(!bFreeMouseCam) +#endif + m_fCarGunLR -= CPad::GetPad(0)->GetCarGunLeftRight() * 0.00015f * CTimer::GetTimeStep(); + if(m_fCarGunLR < 0.0f) m_fCarGunLR += TWOPI; if(m_fCarGunLR > TWOPI) From 409663adb8cd5b6d4ad732ec9713657a1bae9e04 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Tue, 31 Mar 2020 13:30:13 +0300 Subject: [PATCH 60/70] timebars --- src/core/Frontend.cpp | 5 ++ src/core/Frontend.h | 6 +++ src/core/Timer.cpp | 5 ++ src/core/Timer.h | 1 + src/core/config.h | 3 +- src/core/main.cpp | 75 +++++++++++++++++++++++++- src/core/timebars.cpp | 121 ++++++++++++++++++++++++++++++++++++++++++ src/core/timebars.h | 6 +++ 8 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 src/core/timebars.cpp create mode 100644 src/core/timebars.h diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 09759c28..4fefe9a9 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -96,6 +96,11 @@ int32 &JoyButtonJustClicked = *(int32*)0x628D10; bool &holdingScrollBar = *(bool*)0x628D59; //int32 *pControlTemp = 0; +#ifndef MASTER +bool CMenuManager::m_PrefsMarketing = false; +bool CMenuManager::m_PrefsDisableTutorials = false; +#endif // !MASTER + // 0x5F311C const char* FrontendFilenames[][2] = { {"fe2_mainpanel_ul", "" }, diff --git a/src/core/Frontend.h b/src/core/Frontend.h index b7215fa4..30e4f652 100644 --- a/src/core/Frontend.h +++ b/src/core/Frontend.h @@ -497,6 +497,12 @@ public: static int32 &sthWithButtons; static int32 &sthWithButtons2; +#ifndef MASTER + static bool m_PrefsMarketing; + static bool m_PrefsDisableTutorials; +#endif // !MASTER + + public: static void BuildStatLine(char *text, void *stat, uint8 aFloat, void *stat2); static void CentreMousePointer(); diff --git a/src/core/Timer.cpp b/src/core/Timer.cpp index a46e1d8b..18d6b6a3 100644 --- a/src/core/Timer.cpp +++ b/src/core/Timer.cpp @@ -214,6 +214,11 @@ void CTimer::EndUserPause(void) m_UserPause = false; } +uint32 CTimer::GetCyclesPerFrame() +{ + return 20; +} + #if 1 STARTPATCHES InjectHook(0x4ACE60, CTimer::Initialise, PATCH_JUMP); diff --git a/src/core/Timer.h b/src/core/Timer.h index ef525be7..2498ec8a 100644 --- a/src/core/Timer.h +++ b/src/core/Timer.h @@ -34,6 +34,7 @@ public: static void SetPreviousTimeInMilliseconds(uint32 t) { m_snPreviousTimeInMilliseconds = t; } static const float &GetTimeScale(void) { return ms_fTimeScale; } static void SetTimeScale(float ts) { ms_fTimeScale = ts; } + static uint32 GetCyclesPerFrame(); static bool GetIsPaused() { return m_UserPause || m_CodePause; } static bool GetIsUserPaused() { return m_UserPause; } diff --git a/src/core/config.h b/src/core/config.h index b4f3b7b2..58885e57 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -169,8 +169,9 @@ enum Config { // not in any game # define NASTY_GAME // nasty game for all languages # define NO_MOVIES // disable intro videos -# define NO_CDCHECK +# define NO_CDCHECK # define CHATTYSPLASH // print what the game is loading +//# define TIMEBARS // print debug timers #endif #define FIX_BUGS // fixes bugs that we've came across during reversing, TODO: use this more diff --git a/src/core/main.cpp b/src/core/main.cpp index 50543b1e..674527f5 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -51,6 +51,7 @@ #include "Script.h" #include "Debug.h" #include "Console.h" +#include "timebars.h" #define DEFAULT_VIEWWINDOW (Tan(DEGTORAD(CDraw::GetFOV() * 0.5f))) @@ -141,6 +142,11 @@ Idle(void *arg) #endif CTimer::Update(); + +#ifdef TIMEBARS + tbInit(); +#endif + CSprite2d::InitPerFrame(); CFont::InitPerFrame(); @@ -155,16 +161,39 @@ Idle(void *arg) FrontEndMenuManager.Process(); } else { CPointLights::InitPerFrame(); +#ifdef TIMEBARS + tbStartTimer(0, "CGame::Process"); +#endif CGame::Process(); +#ifdef TIMEBARS + tbEndTimer("CGame::Process"); + tbStartTimer(0, "DMAudio.Service"); +#endif DMAudio.Service(); + +#ifdef TIMEBARS + tbEndTimer("DMAudio.Service"); +#endif } if (RsGlobal.quit) return; #else CPointLights::InitPerFrame(); +#ifdef TIMEBARS + tbStartTimer(0, "CGame::Process"); +#endif CGame::Process(); +#ifdef TIMEBARS + tbEndTimer("CGame::Process"); + tbStartTimer(0, "DMAudio.Service"); +#endif + DMAudio.Service(); + +#ifdef TIMEBARS + tbEndTimer("DMAudio.Service"); +#endif #endif if(CGame::bDemoMode && CTimer::GetTimeInMilliseconds() > (3*60 + 30)*1000 && !CCutsceneMgr::IsCutsceneProcessing()){ @@ -191,9 +220,19 @@ Idle(void *arg) pos.y = SCREEN_HEIGHT / 2.0f; RsMouseSetPos(&pos); } +#endif +#ifdef TIMEBARS + tbStartTimer(0, "CnstrRenderList"); #endif CRenderer::ConstructRenderList(); +#ifdef TIMEBARS + tbEndTimer("CnstrRenderList"); + tbStartTimer(0, "PreRender"); +#endif CRenderer::PreRender(); +#ifdef TIMEBARS + tbEndTimer("PreRender"); +#endif if(CWeather::LightningFlash && !CCullZones::CamNoRain()){ if(!DoRWStuffStartOfFrame_Horizon(255, 255, 255, 255, 255, 255, 255)) @@ -211,16 +250,31 @@ Idle(void *arg) RwCameraSetFarClipPlane(Scene.camera, CTimeCycle::GetFarClip()); RwCameraSetFogDistance(Scene.camera, CTimeCycle::GetFogStart()); +#ifdef TIMEBARS + tbStartTimer(0, "RenderScene"); +#endif RenderScene(); +#ifdef TIMEBARS + tbEndTimer("RenderScene"); +#endif RenderDebugShit(); RenderEffects(); +#ifdef TIMEBARS + tbStartTimer(0, "RenderMotionBlur"); +#endif if((TheCamera.m_BlurType == MBLUR_NONE || TheCamera.m_BlurType == MBLUR_NORMAL) && TheCamera.m_ScreenReductionPercentage > 0.0f) TheCamera.SetMotionBlurAlpha(150); TheCamera.RenderMotionBlur(); - +#ifdef TIMEBARS + tbEndTimer("RenderMotionBlur"); + tbStartTimer(0, "Render2dStuff"); +#endif Render2dStuff(); +#ifdef TIMEBARS + tbEndTimer("Render2dStuff"); +#endif }else{ float viewWindow = DEFAULT_VIEWWINDOW; #ifdef ASPECT_RATIO_SCALE @@ -237,11 +291,30 @@ Idle(void *arg) #ifdef PS2_SAVE_DIALOG if (FrontEndMenuManager.m_bMenuActive) DefinedState(); +#endif +#ifdef TIMEBARS + tbStartTimer(0, "RenderMenus"); #endif RenderMenus(); +#ifdef TIMEBARS + tbEndTimer("RenderMenus"); + tbStartTimer(0, "DoFade"); +#endif DoFade(); +#ifdef TIMEBARS + tbEndTimer("DoFade"); + tbStartTimer(0, "Render2dStuff-Fade"); +#endif Render2dStuffAfterFade(); +#ifdef TIMEBARS + tbEndTimer("Render2dStuff-Fade"); +#endif CCredits::Render(); + +#ifdef TIMEBARS + tbDisplay(); +#endif + DoRWStuffEndOfFrame(); // if(g_SlowMode) diff --git a/src/core/timebars.cpp b/src/core/timebars.cpp new file mode 100644 index 00000000..30421731 --- /dev/null +++ b/src/core/timebars.cpp @@ -0,0 +1,121 @@ +#ifndef MASTER +#include "common.h" +#include "Font.h" +#include "Frontend.h" +#include "Timer.h" +#include "Text.h" + +#define MAX_TIMERS (50) +#define MAX_MS_COLLECTED (40) + +// enables frame time output +#define FRAMETIME + +struct sTimeBar +{ + char name[20]; + float startTime; + float endTime; + int32 unk; +}; + +struct +{ + sTimeBar Timers[MAX_TIMERS]; + uint32 count; +} TimerBar; +float MaxTimes[MAX_TIMERS]; +float MaxFrameTime; + +uint32 curMS; +uint32 msCollected[MAX_MS_COLLECTED]; +#ifdef FRAMETIME +float FrameInitTime; +#endif + +void tbInit() +{ + TimerBar.count = 0; + uint32 i = CTimer::GetFrameCounter() & 0x7F; + if (i == 0) { + do + MaxTimes[i++] = 0.0f; + while (i != MAX_TIMERS); +#ifdef FRAMETIME + MaxFrameTime = 0.0f; +#endif + } +#ifdef FRAMETIME + FrameInitTime = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerFrame(); +#endif +} + +void tbStartTimer(int32 unk, char *name) +{ + strcpy(TimerBar.Timers[TimerBar.count].name, name); + TimerBar.Timers[TimerBar.count].unk = unk; + TimerBar.Timers[TimerBar.count].startTime = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerFrame(); + TimerBar.count++; +} + +void tbEndTimer(char* name) +{ + uint32 n = 1500; + for (uint32 i = 0; i < TimerBar.count; i++) { + if (strcmp(name, TimerBar.Timers[i].name) == 0) + n = i; + } + assert(n != 1500); + TimerBar.Timers[n].endTime = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerFrame(); +} + +float Diag_GetFPS() +{ + return 39000.0f / (msCollected[(curMS - 1) % MAX_MS_COLLECTED] - msCollected[curMS % MAX_MS_COLLECTED]); +} + +void tbDisplay() +{ + char temp[200]; + wchar wtemp[200]; + +#ifdef FRAMETIME + float FrameEndTime = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerFrame(); +#endif + + msCollected[(curMS++) % MAX_MS_COLLECTED] = RsTimer(); + CFont::SetBackgroundOff(); + CFont::SetBackgroundColor(CRGBA(0, 0, 0, 128)); + CFont::SetScale(0.48f, 1.12f); + CFont::SetCentreOff(); + CFont::SetJustifyOff(); + CFont::SetWrapx(640.0f); + CFont::SetRightJustifyOff(); + CFont::SetPropOn(); + CFont::SetFontStyle(FONT_BANK); + sprintf(temp, "FPS: %.2f", Diag_GetFPS()); + AsciiToUnicode(temp, wtemp); + CFont::SetColor(CRGBA(255, 255, 255, 255)); + if (!CMenuManager::m_PrefsMarketing || !CMenuManager::m_PrefsDisableTutorials) { + CFont::PrintString(RsGlobal.maximumWidth * (4.0f / DEFAULT_SCREEN_WIDTH), RsGlobal.maximumHeight * (4.0f / DEFAULT_SCREEN_HEIGHT), wtemp); + +#ifndef FINAL + // Timers output (my own implementation) + for (uint32 i = 0; i < TimerBar.count; i++) { + MaxTimes[i] = max(MaxTimes[i], TimerBar.Timers[i].endTime - TimerBar.Timers[i].startTime); + sprintf(temp, "%s: %.2f", &TimerBar.Timers[i].name[0], MaxTimes[i]); + AsciiToUnicode(temp, wtemp); + CFont::PrintString(RsGlobal.maximumWidth * (4.0f / DEFAULT_SCREEN_WIDTH), RsGlobal.maximumHeight * ((8.0f * (i + 2)) / DEFAULT_SCREEN_HEIGHT), wtemp); + } + +#ifdef FRAMETIME + MaxFrameTime = max(MaxFrameTime, FrameEndTime - FrameInitTime); + sprintf(temp, "Frame Time: %.2f", MaxFrameTime); + AsciiToUnicode(temp, wtemp); + + CFont::PrintString(RsGlobal.maximumWidth * (4.0f / DEFAULT_SCREEN_WIDTH), RsGlobal.maximumHeight * ((8.0f * (TimerBar.count + 4)) / DEFAULT_SCREEN_HEIGHT), wtemp); +#endif // FRAMETIME +#endif // !FINAL + } +} +#endif // !MASTER \ No newline at end of file diff --git a/src/core/timebars.h b/src/core/timebars.h new file mode 100644 index 00000000..8ffccd8e --- /dev/null +++ b/src/core/timebars.h @@ -0,0 +1,6 @@ +#pragma once + +void tbInit(); +void tbStartTimer(int32, char*); +void tbEndTimer(char*); +void tbDisplay(); \ No newline at end of file From e34631adcea643d10c23a3f108cddf5ed95c2442 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Wed, 1 Apr 2020 00:07:09 +0300 Subject: [PATCH 61/70] review fixes --- src/control/Garages.cpp | 180 ++++++++++++++-------------------------- src/control/Garages.h | 5 +- 2 files changed, 68 insertions(+), 117 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index aabad1a6..93857b14 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -119,7 +119,7 @@ int32(&CGarages::CarTypesCollected)[TOTAL_COLLECTCARS_GARAGES] = *(int32(*)[TOTA int32& CGarages::CrushedCarId = *(int32*)0x943060; uint32& CGarages::LastTimeHelpMessage = *(uint32*)0x8F1B58; int32& CGarages::MessageNumberInString = *(int32*)0x885BA8; -char(&CGarages::MessageIDString)[8] = *(char(*)[8]) * (uintptr*)0x878358; +char(&CGarages::MessageIDString)[MESSAGE_LENGTH] = *(char(*)[MESSAGE_LENGTH]) * (uintptr*)0x878358; int32& CGarages::MessageNumberInString2 = *(int32*)0x8E2C14; uint32& CGarages::MessageStartTime = *(uint32*)0x8F2530; uint32& CGarages::MessageEndTime = *(uint32*)0x8F597C; @@ -467,8 +467,8 @@ void CGarage::Update() if (IsPlayerOutsideGarage()) m_eGarageState = GS_OPENED; break; - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -571,8 +571,8 @@ void CGarage::Update() if (IsPlayerOutsideGarage()) m_eGarageState = GS_OPENED; break; - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -631,9 +631,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -715,9 +715,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -754,7 +754,7 @@ void CGarage::Update() TheCamera.SetCameraDirectlyBehindForFollowPed_CamOnAString(); } } - } + } break; case GS_CLOSING: m_fDoorPos = max(0.0f, m_fDoorPos - (m_bRotatedDoor ? ROTATED_DOOR_CLOSE_SPEED : DEFAULT_DOOR_CLOSE_SPEED) * CTimer::GetTimeStep()); @@ -804,12 +804,12 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; - } + } break; case GARAGE_FORCARTOCOMEOUTOF: switch (m_eGarageState) { @@ -836,9 +836,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -905,10 +905,9 @@ void CGarage::Update() } UpdateCrusherAngle(); break; - //case GS_FULLYCLOSED: - //case GS_CLOSEDCONTAINSCAR: - //case GS_OPENEDCONTAINSCAR: - + //case GS_FULLYCLOSED: + //case GS_CLOSEDCONTAINSCAR: + //case GS_OPENEDCONTAINSCAR: default: break; } @@ -971,8 +970,8 @@ void CGarage::Update() if (m_eGarageType == GARAGE_MISSION_KEEPCAR && CTimer::GetTimeInMilliseconds() > m_nTimeToStartAction) m_eGarageState = GS_OPENING; break; - //case GS_OPENEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -987,12 +986,12 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENED: - //case GS_CLOSING: - //case GS_FULLYCLOSED: - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENED: + //case GS_CLOSING: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -1015,16 +1014,15 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENED: - //case GS_FULLYCLOSED: - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENED: + //case GS_FULLYCLOSED: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } break; - case GARAGE_HIDEOUT_ONE: case GARAGE_HIDEOUT_TWO: case GARAGE_HIDEOUT_THREE: @@ -1103,9 +1101,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -1144,9 +1142,9 @@ void CGarage::Update() } UpdateDoorsHeight(); break; - //case GS_OPENEDCONTAINSCAR: - //case GS_CLOSEDCONTAINSCAR: - //case GS_AFTERDROPOFF: + //case GS_OPENEDCONTAINSCAR: + //case GS_CLOSEDCONTAINSCAR: + //case GS_AFTERDROPOFF: default: break; } @@ -1155,9 +1153,8 @@ void CGarage::Update() //case GARAGE_60SECONDS: default: break; - } - - } + } +} bool CGarage::IsStaticPlayerCarEntirelyInside() { @@ -1170,19 +1167,12 @@ bool CGarage::IsStaticPlayerCarEntirelyInside() if (FindPlayerPed()->m_objective == OBJECTIVE_LEAVE_VEHICLE) return false; CVehicle* pVehicle = FindPlayerVehicle(); - if (pVehicle->GetPosition().x < m_fX1) + if (pVehicle->GetPosition().x < m_fX1 || pVehicle->GetPosition().x > m_fX2 || + pVehicle->GetPosition().y < m_fY1 || pVehicle->GetPosition().y > m_fY2) return false; - if (pVehicle->GetPosition().x > m_fX2) - return false; - if (pVehicle->GetPosition().y < m_fY1) - return false; - if (pVehicle->GetPosition().y > m_fY2) - return false; - if (Abs(pVehicle->GetSpeed().x) > 0.01f) - return false; - if (Abs(pVehicle->GetSpeed().y) > 0.01f) - return false; - if (Abs(pVehicle->GetSpeed().z) > 0.01f) + if (Abs(pVehicle->GetSpeed().x) > 0.01f || + Abs(pVehicle->GetSpeed().y) > 0.01f || + Abs(pVehicle->GetSpeed().z) > 0.01f) return false; if (pVehicle->GetSpeed().MagnitudeSqr() > SQR(0.01f)) return false; @@ -1191,25 +1181,15 @@ bool CGarage::IsStaticPlayerCarEntirelyInside() bool CGarage::IsEntityEntirelyInside(CEntity * pEntity) { - if (pEntity->GetPosition().x < m_fX1) - return false; - if (pEntity->GetPosition().x > m_fX2) - return false; - if (pEntity->GetPosition().y < m_fY1) - return false; - if (pEntity->GetPosition().y > m_fY2) + if (pEntity->GetPosition().x < m_fX1 || pEntity->GetPosition().x > m_fX2 || + pEntity->GetPosition().y < m_fY1 || pEntity->GetPosition().y > m_fY2) return false; CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); for (int i = 0; i < pColModel->numSpheres; i++) { CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; float radius = pColModel->spheres[i].radius; - if (pos.x - radius < m_fX1) - return false; - if (pos.x + radius > m_fX2) - return false; - if (pos.y - radius < m_fY1) - return false; - if (pos.y + radius > m_fY2) + if (pos.x - radius < m_fX1 || pos.x + radius > m_fX2 || + pos.y - radius < m_fY1 || pos.y + radius > m_fY2) return false; } return true; @@ -1217,33 +1197,17 @@ bool CGarage::IsEntityEntirelyInside(CEntity * pEntity) bool CGarage::IsEntityEntirelyInside3D(CEntity * pEntity, float fMargin) { - if (pEntity->GetPosition().x < m_fX1 - fMargin) - return false; - if (pEntity->GetPosition().x > m_fX2 + fMargin) - return false; - if (pEntity->GetPosition().y < m_fY1 - fMargin) - return false; - if (pEntity->GetPosition().y > m_fY2 + fMargin) - return false; - if (pEntity->GetPosition().z < m_fZ1 - fMargin) - return false; - if (pEntity->GetPosition().z > m_fZ2 + fMargin) + if (pEntity->GetPosition().x < m_fX1 - fMargin || pEntity->GetPosition().x > m_fX2 + fMargin || + pEntity->GetPosition().y < m_fY1 - fMargin || pEntity->GetPosition().y > m_fY2 + fMargin || + pEntity->GetPosition().z < m_fZ1 - fMargin || pEntity->GetPosition().z > m_fZ2 + fMargin) return false; CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); for (int i = 0; i < pColModel->numSpheres; i++) { CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; float radius = pColModel->spheres[i].radius; - if (pos.x + radius < m_fX1 - fMargin) - return false; - if (pos.x - radius > m_fX2 + fMargin) - return false; - if (pos.y + radius < m_fY1 - fMargin) - return false; - if (pos.y - radius > m_fY2 + fMargin) - return false; - if (pos.z + radius < m_fZ1 - fMargin) - return false; - if (pos.z - radius > m_fZ2 + fMargin) + if (pos.x + radius < m_fX1 - fMargin || pos.x - radius > m_fX2 + fMargin || + pos.y + radius < m_fY1 - fMargin || pos.y - radius > m_fY2 + fMargin || + pos.z + radius < m_fZ1 - fMargin || pos.z - radius > m_fZ2 + fMargin) return false; } return true; @@ -1282,17 +1246,9 @@ bool CGarage::IsPlayerOutsideGarage() bool CGarage::IsEntityTouching3D(CEntity * pEntity) { float radius = pEntity->GetBoundRadius(); - if (pEntity->GetPosition().x - radius < m_fX1) - return false; - if (pEntity->GetPosition().x + radius > m_fX2) - return false; - if (pEntity->GetPosition().y - radius < m_fY1) - return false; - if (pEntity->GetPosition().y + radius > m_fY2) - return false; - if (pEntity->GetPosition().z - radius < m_fZ1) - return false; - if (pEntity->GetPosition().z + radius > m_fZ2) + if (pEntity->GetPosition().x - radius < m_fX1 || pEntity->GetPosition().x + radius > m_fX2 || + pEntity->GetPosition().y - radius < m_fY1 || pEntity->GetPosition().y + radius > m_fY2 || + pEntity->GetPosition().z - radius < m_fZ1 || pEntity->GetPosition().z + radius > m_fZ2) return false; CColModel* pColModel = CModelInfo::GetModelInfo(pEntity->GetModelIndex())->GetColModel(); for (int i = 0; i < pColModel->numSpheres; i++) { @@ -1312,17 +1268,9 @@ bool CGarage::EntityHasASphereWayOutsideGarage(CEntity * pEntity, float fMargin) for (int i = 0; i < pColModel->numSpheres; i++) { CVector pos = pEntity->GetMatrix() * pColModel->spheres[i].center; float radius = pColModel->spheres[i].radius; - if (pos.x + radius + fMargin < m_fX1) - return true; - if (pos.x - radius - fMargin > m_fX2) - return true; - if (pos.y + radius + fMargin < m_fY1) - return true; - if (pos.y - radius - fMargin > m_fY2) - return true; - if (pos.z + radius + fMargin < m_fZ1) - return true; - if (pos.z - radius - fMargin > m_fZ2) + if (pos.x + radius + fMargin < m_fX1 || pos.x - radius - fMargin > m_fX2 || + pos.y + radius + fMargin < m_fY1 || pos.y - radius - fMargin > m_fY2 || + pos.z + radius + fMargin < m_fZ1 || pos.z - radius - fMargin > m_fZ2) return true; } return false; diff --git a/src/control/Garages.h b/src/control/Garages.h index 8b88359a..3f471555 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -174,6 +174,9 @@ static_assert(sizeof(CGarage) == 140, "CGarage"); class CGarages { + enum { + MESSAGE_LENGTH = 8 + }; static int32 &BankVansCollected; static bool &BombsAreFree; static bool &RespraysAreFree; @@ -182,7 +185,7 @@ class CGarages static int32 &CrushedCarId; static uint32 &LastTimeHelpMessage; static int32 &MessageNumberInString; - static char(&MessageIDString)[8]; + static char(&MessageIDString)[MESSAGE_LENGTH]; static int32 &MessageNumberInString2; static uint32 &MessageStartTime; static uint32 &MessageEndTime; From 347f0a0e9c687a198ba7fd0ead67a2d63b7880f2 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Wed, 1 Apr 2020 01:58:40 +0300 Subject: [PATCH 62/70] vehicles missing functions + fixes --- src/control/Garages.cpp | 12 ++- src/control/Pickups.cpp | 29 +++--- src/vehicles/Vehicle.cpp | 188 ++++++++++++++++++++++++++++++++++++++- src/vehicles/Vehicle.h | 3 +- src/weapons/Weapon.cpp | 1 + src/weapons/Weapon.h | 2 + 6 files changed, 207 insertions(+), 28 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 93857b14..68d58b10 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -2078,14 +2078,12 @@ void CGarage::CenterCarInGarage(CVehicle* pVehicle) return; if (IsAnyOtherPedTouchingGarage(FindPlayerPed())) return; - float posX = pVehicle->GetPosition().x; - float posY = pVehicle->GetPosition().y; - float posZ = pVehicle->GetPosition().z; + CVector pos = pVehicle->GetPosition(); float garageX = GetGarageCenterX(); float garageY = GetGarageCenterY(); - float offsetX = garageX - posX; - float offsetY = garageY - posY; - float offsetZ = posZ - posZ; + float offsetX = garageX - pos.x; + float offsetY = garageY - pos.y; + float offsetZ = pos.z - pos.z; float distance = CVector(offsetX, offsetY, offsetZ).Magnitude(); if (distance < RESPRAY_CENTERING_COEFFICIENT) { pVehicle->GetPosition().x = GetGarageCenterX(); @@ -2096,7 +2094,7 @@ void CGarage::CenterCarInGarage(CVehicle* pVehicle) pVehicle->GetPosition().y += offsetY * RESPRAY_CENTERING_COEFFICIENT / distance; } if (!IsEntityEntirelyInside3D(pVehicle, 0.1f)) - pVehicle->GetPosition() = CVector(posX, posY, posZ); + pVehicle->GetPosition() = pos; } void CGarages::CloseHideOutGaragesBeforeSave() diff --git a/src/control/Pickups.cpp b/src/control/Pickups.cpp index b1832f0e..3e3c2a48 100644 --- a/src/control/Pickups.cpp +++ b/src/control/Pickups.cpp @@ -20,6 +20,9 @@ #include "Fire.h" #include "PointLights.h" #include "Pools.h" +#ifdef FIX_BUGS +#include "Replay.h" +#endif #include "Script.h" #include "Shadows.h" #include "SpecialFX.h" @@ -642,32 +645,26 @@ CPickups::AddToCollectedPickupsArray(int32 index) void CPickups::Update() { -#ifndef FIX_BUGS - // BUG: this code can only reach 318 out of 320 pickups +#ifdef FIX_BUGS // RIP speedrunning (solution from SA) + if (CReplay::IsPlayingBack()) + return; +#endif #define PICKUPS_FRAME_SPAN (6) -#define PICKUPS_PER_FRAME (NUMGENERALPICKUPS/PICKUPS_FRAME_SPAN) - - for (uint32 i = PICKUPS_PER_FRAME * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN); i < PICKUPS_PER_FRAME * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN + 1); i++) { +#ifdef FIX_BUGS + for (uint32 i = NUMGENERALPICKUPS * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN) / PICKUPS_FRAME_SPAN; i < NUMGENERALPICKUPS * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN + 1) / PICKUPS_FRAME_SPAN; i++) { +#else // BUG: this code can only reach 318 out of 320 pickups + for (uint32 i = NUMGENERALPICKUPS / PICKUPS_FRAME_SPAN * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN); i < NUMGENERALPICKUPS / PICKUPS_FRAME_SPAN * (CTimer::GetFrameCounter() % PICKUPS_FRAME_SPAN + 1); i++) { +#endif if (aPickUps[i].m_eType != PICKUP_NONE && aPickUps[i].Update(FindPlayerPed(), FindPlayerVehicle(), CWorld::PlayerInFocus)) { AddToCollectedPickupsArray(i); } } - +#undef PICKUPS_FRAME_SPAN for (uint32 i = NUMGENERALPICKUPS; i < NUMPICKUPS; i++) { if (aPickUps[i].m_eType != PICKUP_NONE && aPickUps[i].Update(FindPlayerPed(), FindPlayerVehicle(), CWorld::PlayerInFocus)) { AddToCollectedPickupsArray(i); } } - -#undef PICKUPS_FRAME_SPAN -#undef PICKUPS_PER_FRAME -#else - for (uint32 i = 0; i < NUMPICKUPS; i++) { - if (aPickUps[i].m_eType != PICKUP_NONE && aPickUps[i].Update(FindPlayerPed(), FindPlayerVehicle(), CWorld::PlayerInFocus)) { - AddToCollectedPickupsArray(i); - } - } -#endif } void diff --git a/src/vehicles/Vehicle.cpp b/src/vehicles/Vehicle.cpp index 1fe02953..adeba19e 100644 --- a/src/vehicles/Vehicle.cpp +++ b/src/vehicles/Vehicle.cpp @@ -31,10 +31,17 @@ void *CVehicle::operator new(size_t sz, int handle) { return CPools::GetVehicleP void CVehicle::operator delete(void *p, size_t sz) { CPools::GetVehiclePool()->Delete((CVehicle*)p); } void CVehicle::operator delete(void *p, int handle) { CPools::GetVehiclePool()->Delete((CVehicle*)p); } -WRAPPER bool CVehicle::ShufflePassengersToMakeSpace(void) { EAXJMP(0x5528A0); } -// or Weapon.cpp? -WRAPPER void FireOneInstantHitRound(CVector *shotSource, CVector *shotTarget, int32 damage) { EAXJMP(0x563B00); } -WRAPPER void CVehicle::InflictDamage(CEntity *damagedBy, uint32 weaponType, float damage) { EAXJMP(0x551950); } +#ifdef FIX_BUGS +// I think they meant that +#define DAMAGE_FLEE_IN_CAR_PROBABILITY_VALUE (MYRAND_MAX * 35 / 100) +#define DAMAGE_FLEE_ON_FOOT_PROBABILITY_VALUE (MYRAND_MAX * 70 / 100) +#else +#define DAMAGE_FLEE_IN_CAR_PROBABILITY_VALUE (35000) +#define DAMAGE_FLEE_ON_FOOT_PROBABILITY_VALUE (70000) +#endif +#define DAMAGE_HEALTH_TO_FLEE_ALWAYS (200.0f) +#define DAMAGE_HEALTH_TO_CATCH_FIRE (250.0f) + CVehicle::CVehicle(uint8 CreatedBy) { @@ -361,6 +368,119 @@ CVehicle::ProcessWheelRotation(tWheelState state, const CVector &fwd, const CVec return angularVelocity * CTimer::GetTimeStep(); } +void +CVehicle::InflictDamage(CEntity* damagedBy, eWeaponType weaponType, float damage) +{ + if (!bCanBeDamaged) + return; + if (bOnlyDamagedByPlayer && (damagedBy != FindPlayerPed() && damagedBy != FindPlayerVehicle())) + return; + bool bFrightensDriver = false; + switch (weaponType) { + case WEAPONTYPE_UNARMED: + case WEAPONTYPE_BASEBALLBAT: + if (bMeleeProof) + return; + break; + case WEAPONTYPE_COLT45: + case WEAPONTYPE_UZI: + case WEAPONTYPE_SHOTGUN: + case WEAPONTYPE_AK47: + case WEAPONTYPE_M16: + case WEAPONTYPE_SNIPERRIFLE: + case WEAPONTYPE_TOTAL_INVENTORY_WEAPONS: + case WEAPONTYPE_UZI_DRIVEBY: + if (bBulletProof) + return; + bFrightensDriver = true; + break; + case WEAPONTYPE_ROCKETLAUNCHER: + case WEAPONTYPE_MOLOTOV: + case WEAPONTYPE_GRENADE: + case WEAPONTYPE_EXPLOSION: + if (bExplosionProof) + return; + bFrightensDriver = true; + break; + case WEAPONTYPE_FLAMETHROWER: + if (bFireProof) + return; + break; + case WEAPONTYPE_RAMMEDBYCAR: + if (bCollisionProof) + return; + break; + default: + break; + } + if (m_fHealth > 0.0f) { + if (VehicleCreatedBy == RANDOM_VEHICLE && pDriver && + (m_status == STATUS_SIMPLE || m_status == STATUS_PHYSICS) && + AutoPilot.m_nCarMission == MISSION_CRUISE) { + if (m_randomSeed < DAMAGE_FLEE_IN_CAR_PROBABILITY_VALUE) { + CCarCtrl::SwitchVehicleToRealPhysics(this); + AutoPilot.m_nDrivingStyle = DRIVINGSTYLE_AVOID_CARS; + AutoPilot.m_nCruiseSpeed = GAME_SPEED_TO_CARAI_SPEED * pHandling->Transmission.fUnkMaxVelocity; + m_status = STATUS_PHYSICS; + } + } + m_nLastWeaponDamage = weaponType; + float oldHealth = m_fHealth; + if (m_fHealth > damage) { + m_fHealth -= damage; + if (VehicleCreatedBy == RANDOM_VEHICLE && + (m_fHealth < DAMAGE_HEALTH_TO_FLEE_ALWAYS || + bFrightensDriver && m_randomSeed > DAMAGE_FLEE_ON_FOOT_PROBABILITY_VALUE)) { + switch (m_status) { + case STATUS_SIMPLE: + case STATUS_PHYSICS: + if (pDriver) { + m_status = STATUS_ABANDONED; + pDriver->bFleeAfterExitingCar = true; + pDriver->SetObjective(OBJECTIVE_LEAVE_VEHICLE, this); + } + for (int i = 0; i < m_nNumMaxPassengers; i++) { + if (pPassengers[i]) { + pPassengers[i]->bFleeAfterExitingCar = true; + pPassengers[i]->SetObjective(OBJECTIVE_LEAVE_VEHICLE, this); + } + } + break; + default: + break; + } + } + if (oldHealth > DAMAGE_HEALTH_TO_CATCH_FIRE && m_fHealth < DAMAGE_HEALTH_TO_CATCH_FIRE) { + if (IsCar()) { + CAutomobile* pThisCar = (CAutomobile*)this; + pThisCar->Damage.SetEngineStatus(ENGINE_STATUS_ON_FIRE); + pThisCar->m_pSetOnFireEntity = damagedBy; + if (damagedBy) + damagedBy->RegisterReference((CEntity**)&pThisCar->m_pSetOnFireEntity); + } + } + } + else { + m_fHealth = 0.0f; + if (weaponType == WEAPONTYPE_EXPLOSION) { + // between 1000 and 3047. Also not very nice: can't be saved by respray or cheat + m_nBombTimer = 1000 + CGeneral::GetRandomNumber() & 0x7FF; + m_pBlowUpEntity = damagedBy; + if (damagedBy) + damagedBy->RegisterReference((CEntity**)&m_pBlowUpEntity); + } + else + BlowUpCar(damagedBy); + } + } +#ifdef FIX_BUGS // removing dumb case when shooting police car in player's own garage gives wanted level + if (GetModelIndex() == MI_POLICE && damagedBy == FindPlayerPed() && !bHasBeenOwnedByPlayer) +#else + if (GetModelIndex() == MI_POLICE && damagedBy == FindPlayerPed()) +#endif + FindPlayerPed()->SetWantedLevelNoDrop(1); +} + void CVehicle::ExtinguishCarFire(void) { @@ -375,6 +495,65 @@ CVehicle::ExtinguishCarFire(void) } } +bool +CVehicle::ShufflePassengersToMakeSpace(void) +{ + if (m_nNumPassengers >= m_nNumMaxPassengers) + return false; + if (pPassengers[1] && + !(m_nGettingInFlags & CAR_DOOR_FLAG_LR) && + IsRoomForPedToLeaveCar(COMPONENT_DOOR_REAR_LEFT, nil)) { + if (!pPassengers[2] && !(m_nGettingInFlags & CAR_DOOR_FLAG_RR)) { + pPassengers[2] = pPassengers[1]; + pPassengers[1] = nil; + pPassengers[2]->m_vehEnterType = CAR_DOOR_RR; + return true; + } + if (!pPassengers[0] && !(m_nGettingInFlags & CAR_DOOR_FLAG_RF)) { + pPassengers[0] = pPassengers[1]; + pPassengers[1] = nil; + pPassengers[0]->m_vehEnterType = CAR_DOOR_RF; + return true; + } + return false; + } + if (pPassengers[2] && + !(m_nGettingInFlags & CAR_DOOR_FLAG_RR) && + IsRoomForPedToLeaveCar(COMPONENT_DOOR_REAR_RIGHT, nil)) { + if (!pPassengers[1] && !(m_nGettingInFlags & CAR_DOOR_FLAG_LR)) { + pPassengers[1] = pPassengers[2]; + pPassengers[2] = nil; + pPassengers[1]->m_vehEnterType = CAR_DOOR_LR; + return true; + } + if (!pPassengers[0] && !(m_nGettingInFlags & CAR_DOOR_FLAG_RF)) { + pPassengers[0] = pPassengers[2]; + pPassengers[2] = nil; + pPassengers[0]->m_vehEnterType = CAR_DOOR_RF; + return true; + } + return false; + } + if (pPassengers[0] && + !(m_nGettingInFlags & CAR_DOOR_FLAG_RF) && + IsRoomForPedToLeaveCar(COMPONENT_DOOR_FRONT_RIGHT, nil)) { + if (!pPassengers[1] && !(m_nGettingInFlags & CAR_DOOR_FLAG_LR)) { + pPassengers[1] = pPassengers[0]; + pPassengers[0] = nil; + pPassengers[1]->m_vehEnterType = CAR_DOOR_LR; + return true; + } + if (!pPassengers[2] && !(m_nGettingInFlags & CAR_DOOR_FLAG_RR)) { + pPassengers[2] = pPassengers[0]; + pPassengers[0] = nil; + pPassengers[2]->m_vehEnterType = CAR_DOOR_RR; + return true; + } + return false; + } + return false; +} + void CVehicle::ProcessDelayedExplosion(void) { @@ -831,4 +1010,5 @@ STARTPATCHES InjectHook(0x551EB0, &CVehicle::RemovePassenger, PATCH_JUMP); InjectHook(0x5525A0, &CVehicle::ProcessCarAlarm, PATCH_JUMP); InjectHook(0x552620, &CVehicle::IsSphereTouchingVehicle, PATCH_JUMP); + InjectHook(0x551950, &CVehicle::InflictDamage, PATCH_JUMP); ENDPATCHES diff --git a/src/vehicles/Vehicle.h b/src/vehicles/Vehicle.h index 2ca97841..4639f3e1 100644 --- a/src/vehicles/Vehicle.h +++ b/src/vehicles/Vehicle.h @@ -4,6 +4,7 @@ #include "AutoPilot.h" #include "ModelIndices.h" #include "AnimManager.h" +#include "Weapon.h" class CPed; class CFire; @@ -266,7 +267,7 @@ public: void ProcessCarAlarm(void); bool IsSphereTouchingVehicle(float sx, float sy, float sz, float radius); bool ShufflePassengersToMakeSpace(void); - void InflictDamage(CEntity *damagedBy, uint32 weaponType, float damage); + void InflictDamage(CEntity *damagedBy, eWeaponType weaponType, float damage); bool IsAlarmOn(void) { return m_nAlarmState != 0 && m_nAlarmState != -1; } CVehicleModelInfo* GetModelInfo() { return (CVehicleModelInfo*)CModelInfo::GetModelInfo(GetModelIndex()); } diff --git a/src/weapons/Weapon.cpp b/src/weapons/Weapon.cpp index 09844c23..0f41264f 100644 --- a/src/weapons/Weapon.cpp +++ b/src/weapons/Weapon.cpp @@ -14,6 +14,7 @@ WRAPPER void CWeapon::AddGunshell(CEntity*, CVector const&, CVector2D const&, fl WRAPPER void CWeapon::Update(int32 audioEntity) { EAXJMP(0x563A10); } WRAPPER void CWeapon::DoTankDoomAiming(CEntity *playerVehicle, CEntity *playerPed, CVector *start, CVector *end) { EAXJMP(0x563200); } WRAPPER void CWeapon::InitialiseWeapons(void) { EAXJMP(0x55C2D0); } +WRAPPER void FireOneInstantHitRound(CVector* shotSource, CVector* shotTarget, int32 damage) { EAXJMP(0x563B00); } void CWeapon::Initialise(eWeaponType type, int ammo) diff --git a/src/weapons/Weapon.h b/src/weapons/Weapon.h index 74145564..84760550 100644 --- a/src/weapons/Weapon.h +++ b/src/weapons/Weapon.h @@ -81,3 +81,5 @@ public: static void UpdateWeapons(void); }; static_assert(sizeof(CWeapon) == 0x18, "CWeapon: error"); + +void FireOneInstantHitRound(CVector* shotSource, CVector* shotTarget, int32 damage); \ No newline at end of file From 62ae7245ab8c299f5dd9abd44bd4ba1894e17c8f Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Tue, 31 Mar 2020 16:09:18 +0300 Subject: [PATCH 63/70] AudioScriptObject finished --- src/audio/AudioScriptObject.cpp | 50 ++++++- src/audio/AudioScriptObject.h | 251 ++++++++++++++++---------------- 2 files changed, 174 insertions(+), 127 deletions(-) diff --git a/src/audio/AudioScriptObject.cpp b/src/audio/AudioScriptObject.cpp index b5093c52..0ae3834a 100644 --- a/src/audio/AudioScriptObject.cpp +++ b/src/audio/AudioScriptObject.cpp @@ -4,12 +4,10 @@ #include "Pools.h" #include "DMAudio.h" -WRAPPER void cAudioScriptObject::SaveAllAudioScriptObjects(uint8 *buf, uint32 *size) { EAXJMP(0x57c460); } - void cAudioScriptObject::Reset() { - AudioId = 125; + AudioId = SCRSOUND_INVALID; Posn = CVector(0.0f, 0.0f, 0.0f); AudioEntity = AEHANDLE_NONE; } @@ -19,22 +17,66 @@ cAudioScriptObject::operator new(size_t sz) { return CPools::GetAudioScriptObjectPool()->New(); } + void * cAudioScriptObject::operator new(size_t sz, int handle) { return CPools::GetAudioScriptObjectPool()->New(handle); } + void cAudioScriptObject::operator delete(void *p, size_t sz) { CPools::GetAudioScriptObjectPool()->Delete((cAudioScriptObject *)p); } + void cAudioScriptObject::operator delete(void *p, int handle) { CPools::GetAudioScriptObjectPool()->Delete((cAudioScriptObject *)p); } +void +cAudioScriptObject::LoadAllAudioScriptObjects(uint8 *buf, uint32 size) +{ + INITSAVEBUF + + CheckSaveHeader(buf, 'A', 'U', 'D', '\0', size - SAVE_HEADER_SIZE); + + int32 pool_size = ReadSaveBuf(buf); + for (int32 i = 0; i < pool_size; i++) { + int handle = ReadSaveBuf(buf); + cAudioScriptObject *p = new(handle) cAudioScriptObject; + assert(p != nil); + *p = ReadSaveBuf(buf); + p->AudioEntity = DMAudio.CreateLoopingScriptObject(p); + } + + VALIDATESAVEBUF(size); +} + +void +cAudioScriptObject::SaveAllAudioScriptObjects(uint8 *buf, uint32 *size) +{ + INITSAVEBUF + + int32 pool_size = CPools::GetAudioScriptObjectPool()->GetNoOfUsedSpaces(); + *size = SAVE_HEADER_SIZE + pool_size * (sizeof(cAudioScriptObject) + sizeof(int32)); + WriteSaveHeader(buf, 'A', 'U', 'D', '\0', *size - SAVE_HEADER_SIZE); + WriteSaveBuf(buf, pool_size); + + int32 i = CPools::GetAudioScriptObjectPool()->GetSize(); + while (i--) { + cAudioScriptObject *p = CPools::GetAudioScriptObjectPool()->GetSlot(i); + if (p != nil) { + WriteSaveBuf(buf, CPools::GetAudioScriptObjectPool()->GetIndex(p)); + WriteSaveBuf(buf, *p); + } + } + + VALIDATESAVEBUF(*size); +} + void PlayOneShotScriptObject(uint8 id, CVector const &pos) { @@ -48,4 +90,6 @@ PlayOneShotScriptObject(uint8 id, CVector const &pos) STARTPATCHES InjectHook(0x57C430, &cAudioScriptObject::Reset, PATCH_JUMP); InjectHook(0x57C5F0, &PlayOneShotScriptObject, PATCH_JUMP); +InjectHook(0x57C560, &cAudioScriptObject::LoadAllAudioScriptObjects, PATCH_JUMP); +InjectHook(0x57c460, &cAudioScriptObject::SaveAllAudioScriptObjects, PATCH_JUMP); ENDPATCHES \ No newline at end of file diff --git a/src/audio/AudioScriptObject.h b/src/audio/AudioScriptObject.h index 1db19865..4308faee 100644 --- a/src/audio/AudioScriptObject.h +++ b/src/audio/AudioScriptObject.h @@ -2,130 +2,132 @@ enum { - SCRSOUND_TEST_1 = 0, - _SCRSOUND_UNK_1 = 1, - _SCRSOUND_UNK_2 = 2, - _SCRSOUND_UNK_3 = 3, - _SCRSOUND_CLUB_1_S = 4, - _SCRSOUND_CLUB_1_L = 5, - _SCRSOUND_CLUB_2_S = 6, - _SCRSOUND_CLUB_2_L = 7, - _SCRSOUND_CLUB_3_S = 8, - _SCRSOUND_CLUB_3_L = 9, - _SCRSOUND_CLUB_4_S = 10, - _SCRSOUND_CLUB_4_L = 11, - _SCRSOUND_CLUB_5_S = 12, - _SCRSOUND_CLUB_5_L = 13, - _SCRSOUND_CLUB_6_S = 14, - _SCRSOUND_CLUB_6_L = 15, - _SCRSOUND_CLUB_7_S = 16, - _SCRSOUND_CLUB_7_L = 17, - _SCRSOUND_CLUB_8_S = 18, - _SCRSOUND_CLUB_8_L = 19, - _SCRSOUND_CLUB_9_S = 20, - _SCRSOUND_CLUB_9_L = 21, - _SCRSOUND_CLUB_10_S = 22, - _SCRSOUND_CLUB_10_L = 23, - _SCRSOUND_CLUB_11_S = 24, - _SCRSOUND_CLUB_11_L = 25, - _SCRSOUND_CLUB_12_S = 26, - _SCRSOUND_CLUB_12_L = 27, - _SCRSOUND_CLUB_RAGGA_S = 28, - _SCRSOUND_CLUB_RAGGA_L = 29, - SCRSOUND_STRIP_CLUB_LOOP_1_S = 30, - _SCRSOUND_STRIP_CLUB_LOOP_1_L = 31, - SCRSOUND_STRIP_CLUB_LOOP_2_S = 32, - _SCRSOUND_STRIP_CLUB_LOOP_2_L = 33, - _SCRSOUND_SFX_WORKSHOP_1 = 34, - _SCRSOUND_SFX_WORKSHOP_2 = 35, - _SCRSOUND_SAWMILL_LOOP_S = 36, - SCRSOUND_SAWMILL_LOOP_L = 37, - _SCRSOUND_DOG_FOOD_FACTORY_S = 38, - _SCRSOUND_DOG_FOOD_FACTORY_L = 39, - _SCRSOUND_LAUNDERETTE_1 = 40, - _SCRSOUND_LAUNDERETTE_2 = 41, - _SCRSOUND_RESTAURANT_CHINATOWN_S = 42, - _SCRSOUND_RESTAURANT_CHINATOWN_L = 43, - _SCRSOUND_RESTAURANT_ITALY_S = 44, - _SCRSOUND_RESTAURANT_ITALY_L = 45, - _SCRSOUND_RESTAURANT_GENERIC_1_S = 46, - _SCRSOUND_RESTAURANT_GENERIC_1_L = 47, - _SCRSOUND_RESTAURANT_GENERIC_2_S = 48, - _SCRSOUND_RESTAURANT_GENERIC_2_L = 49, - _SCRSOUND_AIRPORT_ANNOUNCEMENT_S = 50, - _SCRSOUND_AIRPORT_ANNOUNCEMENT_L = 51, - _SCRSOUND_SHOP_LOOP_1 = 52, - _SCRSOUND_SHOP_LOOP_2 = 53, - _SCRSOUND_CINEMA_S = 54, - _SCRSOUND_CINEMA_L = 55, - _SCRSOUND_DOCKS_FOGHORN_S = 56, - _SCRSOUND_DOCKS_FOGHORN_L = 57, - _SCRSOUND_HOME_S = 58, - _SCRSOUND_HOME_L = 59, - _SCRSOUND_PIANO_BAR = 60, - _SCRSOUND_CLUB = 61, - SCRSOUND_PORN_CINEMA_1_S = 62, - _SCRSOUND_PORN_CINEMA_1_L = 63, - SCRSOUND_PORN_CINEMA_2_S = 64, - _SCRSOUND_PORN_CINEMA_2_L = 65, - SCRSOUND_PORN_CINEMA_3_S = 66, - _SCRSOUND_PORN_CINEMA_3_L = 67, - _SCRSOUND_BANK_ALARM_LOOP_S = 68, - SCRSOUND_BANK_ALARM_LOOP_L = 69, - _SCRSOUND_POLICE_BALL_LOOP_S = 70, - SCRSOUND_POLICE_BALL_LOOP_L = 71, - _SCRSOUND_RAVE_LOOP_INDUSTRIAL_S = 72, - SCRSOUND_RAVE_LOOP_INDUSTRIAL_L = 73, - _SCRSOUND_UNK_74 = 74, - _SCRSOUND_UNK_75 = 75, - _SCRSOUND_POLICE_CELL_BEATING_LOOP_S = 76, - SCRSOUND_POLICE_CELL_BEATING_LOOP_L = 77, - SCRSOUND_INJURED_PED_MALE_OUCH_S = 78, - SCRSOUND_INJURED_PED_MALE_OUCH_L = 79, - SCRSOUND_INJURED_PED_FEMALE_OUCH_S = 80, - SCRSOUND_INJURED_PED_FEMALE_OUCH_L = 81, - SCRSOUND_EVIDENCE_PICKUP = 82, - SCRSOUND_UNLOAD_GOLD = 83, - _SCRSOUND_RAVE_INDUSTRIAL_S = 84, - _SCRSOUND_RAVE_INDUSTRIAL_L = 85, - _SCRSOUND_RAVE_COMMERCIAL_S = 86, - _SCRSOUND_RAVE_COMMERCIAL_L = 87, - _SCRSOUND_RAVE_SUBURBAN_S = 88, - _SCRSOUND_RAVE_SUBURBAN_L = 89, - _SCRSOUND_GROAN_S = 90, - _SCRSOUND_GROAN_L = 91, - SCRSOUND_GATE_START_CLUNK = 92, - SCRSOUND_GATE_STOP_CLUNK = 93, - SCRSOUND_PART_MISSION_COMPLETE = 94, - SCRSOUND_CHUNKY_RUN_SHOUT = 95, - SCRSOUND_SECURITY_GUARD_RUN_AWAY_SHOUT = 96, - SCRSOUND_RACE_START_1 = 97, - SCRSOUND_RACE_START_2 = 98, - SCRSOUND_RACE_START_3 = 99, - SCRSOUND_RACE_START_GO = 100, - SCRSOUND_SWAT_PED_SHOUT = 101, - SCRSOUND_PRETEND_FIRE_LOOP = 102, - SCRSOUND_AMMUNATION_CHAT_1 = 103, - SCRSOUND_AMMUNATION_CHAT_2 = 104, - SCRSOUND_AMMUNATION_CHAT_3 = 105, - _SCRSOUND_BULLET_WALL_1 = 106, - _SCRSOUND_BULLET_WALL_2 = 107, - _SCRSOUND_BULLET_WALL_3 = 108, - _SCRSOUND_UNK_109 = 109, - _SCRSOUND_GLASSFX2_1 = 110, - _SCRSOUND_GLASSFX2_2 = 111, - _SCRSOUND_PHONE_RING = 112, - _SCRSOUND_UNK_113 = 113, - _SCRSOUND_GLASS_SMASH_1 = 114, - _SCRSOUND_GLASS_SMASH_2 = 115, - _SCRSOUND_GLASS_CRACK = 116, - _SCRSOUND_GLASS_SHARD = 117, - _SCRSOUND_WOODEN_BOX_SMASH = 118, - _SCRSOUND_CARDBOARD_BOX_SMASH = 119, - _SCRSOUND_COL_CAR = 120, - _SCRSOUND_TYRE_BUMP = 121, - _SCRSOUND_BULLET_SHELL_HIT_GROUND_1 = 122, - _SCRSOUND_BULLET_SHELL_HIT_GROUND_2 = 123, + SCRSOUND_TEST_1, + _SCRSOUND_UNK_1, + _SCRSOUND_UNK_2, + _SCRSOUND_UNK_3, + _SCRSOUND_CLUB_1_S, + _SCRSOUND_CLUB_1_L, + _SCRSOUND_CLUB_2_S, + _SCRSOUND_CLUB_2_L, + _SCRSOUND_CLUB_3_S, + _SCRSOUND_CLUB_3_L, + _SCRSOUND_CLUB_4_S, + _SCRSOUND_CLUB_4_L, + _SCRSOUND_CLUB_5_S, + _SCRSOUND_CLUB_5_L, + _SCRSOUND_CLUB_6_S, + _SCRSOUND_CLUB_6_L, + _SCRSOUND_CLUB_7_S, + _SCRSOUND_CLUB_7_L, + _SCRSOUND_CLUB_8_S, + _SCRSOUND_CLUB_8_L, + _SCRSOUND_CLUB_9_S, + _SCRSOUND_CLUB_9_L, + _SCRSOUND_CLUB_10_S, + _SCRSOUND_CLUB_10_L, + _SCRSOUND_CLUB_11_S, + _SCRSOUND_CLUB_11_L, + _SCRSOUND_CLUB_12_S, + _SCRSOUND_CLUB_12_L, + _SCRSOUND_CLUB_RAGGA_S, + _SCRSOUND_CLUB_RAGGA_L, + SCRSOUND_STRIP_CLUB_LOOP_1_S, + _SCRSOUND_STRIP_CLUB_LOOP_1_L, + SCRSOUND_STRIP_CLUB_LOOP_2_S, + _SCRSOUND_STRIP_CLUB_LOOP_2_L, + _SCRSOUND_SFX_WORKSHOP_1, + _SCRSOUND_SFX_WORKSHOP_2, + _SCRSOUND_SAWMILL_LOOP_S, + SCRSOUND_SAWMILL_LOOP_L, + _SCRSOUND_DOG_FOOD_FACTORY_S, + _SCRSOUND_DOG_FOOD_FACTORY_L, + _SCRSOUND_LAUNDERETTE_1, + _SCRSOUND_LAUNDERETTE_2, + _SCRSOUND_RESTAURANT_CHINATOWN_S, + _SCRSOUND_RESTAURANT_CHINATOWN_L, + _SCRSOUND_RESTAURANT_ITALY_S, + _SCRSOUND_RESTAURANT_ITALY_L, + _SCRSOUND_RESTAURANT_GENERIC_1_S, + _SCRSOUND_RESTAURANT_GENERIC_1_L, + _SCRSOUND_RESTAURANT_GENERIC_2_S, + _SCRSOUND_RESTAURANT_GENERIC_2_L, + _SCRSOUND_AIRPORT_ANNOUNCEMENT_S, + _SCRSOUND_AIRPORT_ANNOUNCEMENT_L, + _SCRSOUND_SHOP_LOOP_1, + _SCRSOUND_SHOP_LOOP_2, + _SCRSOUND_CINEMA_S, + _SCRSOUND_CINEMA_L, + _SCRSOUND_DOCKS_FOGHORN_S, + _SCRSOUND_DOCKS_FOGHORN_L, + _SCRSOUND_HOME_S, + _SCRSOUND_HOME_L, + _SCRSOUND_PIANO_BAR, + _SCRSOUND_CLUB, + SCRSOUND_PORN_CINEMA_1_S, + _SCRSOUND_PORN_CINEMA_1_L, + SCRSOUND_PORN_CINEMA_2_S, + _SCRSOUND_PORN_CINEMA_2_L, + SCRSOUND_PORN_CINEMA_3_S, + _SCRSOUND_PORN_CINEMA_3_L, + _SCRSOUND_BANK_ALARM_LOOP_S, + SCRSOUND_BANK_ALARM_LOOP_L, + _SCRSOUND_POLICE_BALL_LOOP_S, + SCRSOUND_POLICE_BALL_LOOP_L, + _SCRSOUND_RAVE_LOOP_INDUSTRIAL_S, + SCRSOUND_RAVE_LOOP_INDUSTRIAL_L, + _SCRSOUND_UNK_74, + _SCRSOUND_UNK_75, + _SCRSOUND_POLICE_CELL_BEATING_LOOP_S, + SCRSOUND_POLICE_CELL_BEATING_LOOP_L, + SCRSOUND_INJURED_PED_MALE_OUCH_S, + SCRSOUND_INJURED_PED_MALE_OUCH_L, + SCRSOUND_INJURED_PED_FEMALE_OUCH_S, + SCRSOUND_INJURED_PED_FEMALE_OUCH_L, + SCRSOUND_EVIDENCE_PICKUP, + SCRSOUND_UNLOAD_GOLD, + _SCRSOUND_RAVE_INDUSTRIAL_S, + _SCRSOUND_RAVE_INDUSTRIAL_L, + _SCRSOUND_RAVE_COMMERCIAL_S, + _SCRSOUND_RAVE_COMMERCIAL_L, + _SCRSOUND_RAVE_SUBURBAN_S, + _SCRSOUND_RAVE_SUBURBAN_L, + _SCRSOUND_GROAN_S, + _SCRSOUND_GROAN_L, + SCRSOUND_GATE_START_CLUNK, + SCRSOUND_GATE_STOP_CLUNK, + SCRSOUND_PART_MISSION_COMPLETE, + SCRSOUND_CHUNKY_RUN_SHOUT, + SCRSOUND_SECURITY_GUARD_RUN_AWAY_SHOUT, + SCRSOUND_RACE_START_1, + SCRSOUND_RACE_START_2, + SCRSOUND_RACE_START_3, + SCRSOUND_RACE_START_GO, + SCRSOUND_SWAT_PED_SHOUT, + SCRSOUND_PRETEND_FIRE_LOOP, + SCRSOUND_AMMUNATION_CHAT_1, + SCRSOUND_AMMUNATION_CHAT_2, + SCRSOUND_AMMUNATION_CHAT_3, + _SCRSOUND_BULLET_WALL_1, + _SCRSOUND_BULLET_WALL_2, + _SCRSOUND_BULLET_WALL_3, + _SCRSOUND_UNK_109, + _SCRSOUND_GLASSFX2_1, + _SCRSOUND_GLASSFX2_2, + _SCRSOUND_PHONE_RING, + _SCRSOUND_UNK_113, + _SCRSOUND_GLASS_SMASH_1, + _SCRSOUND_GLASS_SMASH_2, + _SCRSOUND_GLASS_CRACK, + _SCRSOUND_GLASS_SHARD, + _SCRSOUND_WOODEN_BOX_SMASH, + _SCRSOUND_CARDBOARD_BOX_SMASH, + _SCRSOUND_COL_CAR, + _SCRSOUND_TYRE_BUMP, + _SCRSOUND_BULLET_SHELL_HIT_GROUND_1, + _SCRSOUND_BULLET_SHELL_HIT_GROUND_2, + TOTAL_SCRSOUNDS, + SCRSOUND_INVALID }; class cAudioScriptObject @@ -142,6 +144,7 @@ public: static void operator delete(void*, size_t); static void operator delete(void*, int); + static void LoadAllAudioScriptObjects(uint8 *buf, uint32 size); static void SaveAllAudioScriptObjects(uint8 *buf, uint32 *size); }; From 13fb853fd832d4ba92983c968856ba04e8e13dcb Mon Sep 17 00:00:00 2001 From: Fire-Head Date: Thu, 2 Apr 2020 00:04:56 +0300 Subject: [PATCH 64/70] Glass done --- src/core/common.h | 1 + src/core/config.h | 2 + src/objects/Object.cpp | 4 +- src/objects/Object.h | 4 +- src/render/Glass.cpp | 720 ++++++++++++++++++++++++++++++++++++++++- src/render/Glass.h | 53 ++- src/render/Shadows.h | 1 + 7 files changed, 764 insertions(+), 21 deletions(-) diff --git a/src/core/common.h b/src/core/common.h index 7b4ff4a0..b58b93af 100644 --- a/src/core/common.h +++ b/src/core/common.h @@ -215,6 +215,7 @@ void re3_assert(const char *expr, const char *filename, unsigned int lineno, con #define ABS(a) (((a) < 0) ? (-(a)) : (a)) #define norm(value, min, max) (((value) < (min)) ? 0 : (((value) > (max)) ? 1 : (((value) - (min)) / ((max) - (min))))) +#define lerp(norm, min, max) ( (norm) * ((max) - (min)) + (min) ) #define STRINGIFY(x) #x #define STR(x) STRINGIFY(x) diff --git a/src/core/config.h b/src/core/config.h index 0d39550a..926cb863 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -94,6 +94,8 @@ enum Config { NUM_GARAGES = 32, NUM_PROJECTILES = 32, + NUM_GLASSPANES = 45, + NUM_GLASSENTITIES = 32, NUM_WATERCANNONS = 3, NUMPEDROUTES = 200, diff --git a/src/objects/Object.cpp b/src/objects/Object.cpp index 89959975..aa366aa0 100644 --- a/src/objects/Object.cpp +++ b/src/objects/Object.cpp @@ -38,8 +38,8 @@ CObject::CObject(void) bIsPickup = false; m_obj_flag2 = false; bOutOfStock = false; - m_obj_flag8 = false; - m_obj_flag10 = false; + bGlassCracked = false; + bGlassBroken = false; bHasBeenDamaged = false; m_nRefModelIndex = -1; bUseVehicleColours = false; diff --git a/src/objects/Object.h b/src/objects/Object.h index 9fcf9c0c..27346e23 100644 --- a/src/objects/Object.h +++ b/src/objects/Object.h @@ -36,8 +36,8 @@ public: int8 bIsPickup : 1; int8 m_obj_flag2 : 1; int8 bOutOfStock : 1; - int8 m_obj_flag8 : 1; - int8 m_obj_flag10 : 1; + int8 bGlassCracked : 1; + int8 bGlassBroken : 1; int8 bHasBeenDamaged : 1; int8 bUseVehicleColours : 1; int8 m_obj_flag80 : 1; diff --git a/src/render/Glass.cpp b/src/render/Glass.cpp index ac04032b..b082a9e1 100644 --- a/src/render/Glass.cpp +++ b/src/render/Glass.cpp @@ -1,21 +1,721 @@ #include "common.h" #include "patcher.h" #include "Glass.h" +#include "Timer.h" +#include "Object.h" +#include "General.h" +#include "AudioScriptObject.h" +#include "World.h" +#include "TimeCycle.h" +#include "Particle.h" +#include "Camera.h" +#include "RenderBuffer.h" +#include "Shadows.h" +#include "ModelIndices.h" +#include "main.h" -WRAPPER void CGlass::AskForObjectToBeRenderedInGlass(CEntity *ent) { EAXJMP(0x5033F0); } -WRAPPER void -CGlass::WindowRespondsToCollision(CEntity *ent, float amount, CVector speed, CVector point, bool foo) +uint32 CGlass::NumGlassEntities; +CEntity *CGlass::apEntitiesToBeRendered[NUM_GLASSENTITIES]; +CFallingGlassPane CGlass::aGlassPanes[NUM_GLASSPANES]; + + +CVector2D CentersWithTriangle[NUM_GLASSTRIANGLES]; +CVector2D CoorsWithTriangle[NUM_GLASSTRIANGLES][3] = { - EAXJMP(0x503F10); + { + CVector2D(0.0f, 0.0f), + CVector2D(0.0f, 1.0f), + CVector2D(0.4f, 0.5f) + }, + + { + CVector2D(0.0f, 1.0f), + CVector2D(1.0f, 1.0f), + CVector2D(0.4f, 0.5f) + }, + + { + CVector2D(0.0f, 0.0f), + CVector2D(0.4f, 0.5f), + CVector2D(0.7f, 0.0f) + }, + + { + CVector2D(0.7f, 0.0f), + CVector2D(0.4f, 0.5f), + CVector2D(1.0f, 1.0f) + }, + + { + CVector2D(0.7f, 0.0f), + CVector2D(1.0f, 1.0f), + CVector2D(1.0f, 0.0f) + } +}; + +#define TEMPBUFFERVERTHILIGHTOFFSET 0 +#define TEMPBUFFERINDEXHILIGHTOFFSET 0 +#define TEMPBUFFERVERTHILIGHTSIZE 128 +#define TEMPBUFFERINDEXHILIGHTSIZE 512 + +#define TEMPBUFFERVERTSHATTEREDOFFSET TEMPBUFFERVERTHILIGHTSIZE +#define TEMPBUFFERINDEXSHATTEREDOFFSET TEMPBUFFERINDEXHILIGHTSIZE +#define TEMPBUFFERVERTSHATTEREDSIZE 192 +#define TEMPBUFFERINDEXSHATTEREDSIZE 768 + +#define TEMPBUFFERVERTREFLECTIONOFFSET TEMPBUFFERVERTSHATTEREDSIZE +#define TEMPBUFFERINDEXREFLECTIONOFFSET TEMPBUFFERINDEXSHATTEREDSIZE +#define TEMPBUFFERVERTREFLECTIONSIZE 256 +#define TEMPBUFFERINDEXREFLECTIONSIZE 1024 + +int32 TempBufferIndicesStoredHiLight = 0; +int32 TempBufferVerticesStoredHiLight = 0; +int32 TempBufferIndicesStoredShattered = 0; +int32 TempBufferVerticesStoredShattered = 0; +int32 TempBufferIndicesStoredReflection = 0; +int32 TempBufferVerticesStoredReflection = 0; + +void +CFallingGlassPane::Update(void) +{ + if ( CTimer::GetTimeInMilliseconds() >= m_nTimer ) + { + // Apply MoveSpeed + GetPosition() += m_vecMoveSpeed * CTimer::GetTimeStep(); + + // Apply Gravity + m_vecMoveSpeed.z -= 0.02f * CTimer::GetTimeStep(); + + // Apply TurnSpeed + GetRight() += CrossProduct(m_vecTurn, GetRight()); + GetForward() += CrossProduct(m_vecTurn, GetForward()); + GetUp() += CrossProduct(m_vecTurn, GetUp()); + + if ( GetPosition().z < m_fGroundZ ) + { + CVector pos; + CVector dir; + + m_bActive = false; + + pos = CVector(GetPosition().x, GetPosition().y, m_fGroundZ); + + PlayOneShotScriptObject(_SCRSOUND_GLASS_SHARD, pos); + + RwRGBA color = { 255, 255, 255, 255 }; + + static int32 nFrameGen = 0; + + for ( int32 i = 0; i < 4; i++ ) + { + dir.x = CGeneral::GetRandomNumberInRange(-0.35f, 0.35f); + dir.y = CGeneral::GetRandomNumberInRange(-0.35f, 0.35f); + dir.z = CGeneral::GetRandomNumberInRange(0.05f, 0.20f); + + CParticle::AddParticle(PARTICLE_CAR_DEBRIS, + pos, + dir, + NULL, + CGeneral::GetRandomNumberInRange(0.02f, 0.2f), + color, + CGeneral::GetRandomNumberInRange(-40, 40), + 0, + ++nFrameGen & 3, + 500); + } + } + } } -WRAPPER void -CGlass::WindowRespondsToSoftCollision(CEntity *ent, float amount) +void +CFallingGlassPane::Render(void) { - EAXJMP(0x504630); + float distToCamera = (TheCamera.GetPosition() - GetPosition()).Magnitude(); + + CVector fwdNorm = GetForward(); + fwdNorm.Normalise(); + uint8 alpha = CGlass::CalcAlphaWithNormal(&fwdNorm); + + int32 time = clamp(CTimer::GetTimeInMilliseconds() - m_nTimer, 0, 500); + + uint8 color = int32( float(alpha) * (float(time) / 500) ); + + if ( TempBufferIndicesStoredHiLight >= TEMPBUFFERINDEXHILIGHTSIZE-7 || TempBufferVerticesStoredHiLight >= TEMPBUFFERVERTHILIGHTSIZE-4 ) + CGlass::RenderHiLightPolys(); + + // HiLight Polys + + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 0], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 1], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 2], color, color, color, color); + + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 0], 0.5f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 0], 0.5f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 1], 0.5f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 1], 0.6f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 2], 0.6f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 2], 0.6f); + + ASSERT(m_nTriIndex < NUM_GLASSTRIANGLES); + + CVector2D p0 = CoorsWithTriangle[m_nTriIndex][0] - CentersWithTriangle[m_nTriIndex]; + CVector2D p1 = CoorsWithTriangle[m_nTriIndex][1] - CentersWithTriangle[m_nTriIndex]; + CVector2D p2 = CoorsWithTriangle[m_nTriIndex][2] - CentersWithTriangle[m_nTriIndex]; + CVector v0 = *this * CVector(p0.x, 0.0f, p0.y); + CVector v1 = *this * CVector(p1.x, 0.0f, p1.y); + CVector v2 = *this * CVector(p2.x, 0.0f, p2.y); + + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 0], v0.x, v0.y, v0.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 1], v1.x, v1.y, v1.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredHiLight + 2], v2.x, v2.y, v2.z); + + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 0] = TempBufferVerticesStoredHiLight + 0; + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 1] = TempBufferVerticesStoredHiLight + 1; + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 2] = TempBufferVerticesStoredHiLight + 2; + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 3] = TempBufferVerticesStoredHiLight + 0; + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 4] = TempBufferVerticesStoredHiLight + 2; + TempBufferRenderIndexList[TempBufferIndicesStoredHiLight + 5] = TempBufferVerticesStoredHiLight + 1; + + TempBufferVerticesStoredHiLight += 3; + TempBufferIndicesStoredHiLight += 6; + + if ( m_bShattered ) + { + if ( TempBufferIndicesStoredShattered >= TEMPBUFFERINDEXSHATTEREDSIZE-7 || TempBufferVerticesStoredShattered >= TEMPBUFFERVERTSHATTEREDSIZE-4 ) + CGlass::RenderShatteredPolys(); + + uint8 shatteredColor = 255; + if ( distToCamera > 30.0f ) + shatteredColor = int32((1.0f - (distToCamera - 30.0f) * 4.0f / 40.0f) * 255); + + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], shatteredColor, shatteredColor, shatteredColor, shatteredColor); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], shatteredColor, shatteredColor, shatteredColor, shatteredColor); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], shatteredColor, shatteredColor, shatteredColor, shatteredColor); + + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], 4.0f * CoorsWithTriangle[m_nTriIndex][0].x * m_fStep); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], 4.0f * CoorsWithTriangle[m_nTriIndex][0].y * m_fStep); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], 4.0f * CoorsWithTriangle[m_nTriIndex][1].x * m_fStep); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], 4.0f * CoorsWithTriangle[m_nTriIndex][1].y * m_fStep); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], 4.0f * CoorsWithTriangle[m_nTriIndex][2].x * m_fStep); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], 4.0f * CoorsWithTriangle[m_nTriIndex][2].y * m_fStep); + + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], v0.x, v0.y, v0.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], v1.x, v1.y, v1.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], v2.x, v2.y, v2.z); + + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 0] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 0; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 1] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 1; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 2] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 2; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 3] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 0; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 4] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 2; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 5] = TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET + 1; + + TempBufferIndicesStoredShattered += 6; + TempBufferVerticesStoredShattered += 3; + } } -WRAPPER void CGlass::Render(void) { EAXJMP(0x502350); } -WRAPPER void CGlass::Update(void) { EAXJMP(0x502050); } -WRAPPER void CGlass::Init(void) { EAXJMP(0x501F20); } +void +CGlass::Init(void) +{ + for ( int32 i = 0; i < NUM_GLASSPANES; i++ ) + aGlassPanes[i].m_bActive = false; + + for ( int32 i = 0; i < NUM_GLASSTRIANGLES; i++ ) + CentersWithTriangle[i] = (CoorsWithTriangle[i][0] + CoorsWithTriangle[i][1] + CoorsWithTriangle[i][2]) / 3; +} + +void +CGlass::Update(void) +{ + for ( int32 i = 0; i < NUM_GLASSPANES; i++ ) + { + if ( aGlassPanes[i].m_bActive ) + aGlassPanes[i].Update(); + } +} + +void +CGlass::Render(void) +{ + TempBufferVerticesStoredHiLight = 0; + TempBufferIndicesStoredHiLight = 0; + + TempBufferVerticesStoredShattered = TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferIndicesStoredShattered = TEMPBUFFERINDEXSHATTEREDOFFSET; + + TempBufferVerticesStoredReflection = TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferIndicesStoredReflection = TEMPBUFFERINDEXREFLECTIONOFFSET; + + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)FALSE); + RwRenderStateSet(rwRENDERSTATETEXTUREFILTER, (void *)rwFILTERLINEAR); + RwRenderStateSet(rwRENDERSTATEFOGENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATEFOGCOLOR, (void *)RWRGBALONG(CTimeCycle::GetFogRed(), CTimeCycle::GetFogGreen(), CTimeCycle::GetFogBlue(), 255)); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATEVERTEXALPHAENABLE, (void *)TRUE); + + for ( int32 i = 0; i < NUM_GLASSPANES; i++ ) + { + if ( aGlassPanes[i].m_bActive ) + aGlassPanes[i].Render(); + } + + for ( uint32 i = 0; i < NumGlassEntities; i++ ) + RenderEntityInGlass(apEntitiesToBeRendered[i]); + + NumGlassEntities = 0; + + RenderHiLightPolys(); + RenderShatteredPolys(); + RenderReflectionPolys(); + + RwRenderStateSet(rwRENDERSTATEZWRITEENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATEZTESTENABLE, (void *)TRUE); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDSRCALPHA); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDINVSRCALPHA); + RwRenderStateSet(rwRENDERSTATEFOGENABLE, (void *)FALSE); +} + +CFallingGlassPane * +CGlass::FindFreePane(void) +{ + for ( int32 i = 0; i < NUM_GLASSPANES; i++ ) + { + if ( !aGlassPanes[i].m_bActive ) + return &aGlassPanes[i]; + } + + return NULL; +} + +void +CGlass::GeneratePanesForWindow(uint32 type, CVector pos, CVector up, CVector right, CVector speed, CVector point, + float moveSpeed, bool cracked, bool explosion) +{ + float upLen = up.Magnitude(); + float rightLen = right.Magnitude(); + + float upSteps = upLen + 0.75f; + if ( upSteps < 1.0f ) upSteps = 1.0f; + + float rightSteps = rightLen + 0.75f; + if ( rightSteps < 1.0f ) rightSteps = 1.0f; + + uint32 ysteps = (uint32)upSteps; + if ( ysteps > 3 ) ysteps = 3; + + uint32 xsteps = (uint32)rightSteps; + if ( xsteps > 3 ) xsteps = 3; + + if ( explosion ) + { + if ( ysteps > 1 ) ysteps = 1; + if ( xsteps > 1 ) xsteps = 1; + } + + float upScl = upLen / float(ysteps); + float rightScl = rightLen / float(xsteps); + + bool bZFound; + float groundZ = CWorld::FindGroundZFor3DCoord(pos.x, pos.y, pos.z, &bZFound); + if ( !bZFound ) groundZ = pos.z - 2.0f; + + for ( uint32 y = 0; y < ysteps; y++ ) + { + for ( uint32 x = 0; x < xsteps; x++ ) + { + float stepy = float(y) * upLen / float(ysteps); + float stepx = float(x) * rightLen / float(xsteps); + + for ( int32 i = 0; i < NUM_GLASSTRIANGLES; i++ ) + { + CFallingGlassPane *pane = FindFreePane(); + if ( pane ) + { + pane->m_nTriIndex = i; + + pane->GetRight() = (right * rightScl) / rightLen; +#ifdef FIX_BUGS + pane->GetUp() = (up * upScl) / upLen; +#else + pane->GetUp() = (up * upScl) / rightLen; // copypaste bug +#endif + CVector fwd = CrossProduct(pane->GetRight(), pane->GetUp()); + fwd.Normalise(); + + pane->GetForward() = fwd; + + pane->GetPosition() = right / rightLen * (rightScl * CentersWithTriangle[i].x + stepx) + + up / upLen * (upScl * CentersWithTriangle[i].y + stepy) + + pos; + + pane->m_vecMoveSpeed.x = float((CGeneral::GetRandomNumber() & 127) - 64) * 0.0015f + speed.x; + pane->m_vecMoveSpeed.y = float((CGeneral::GetRandomNumber() & 127) - 64) * 0.0015f + speed.y; + pane->m_vecMoveSpeed.z = 0.0f + speed.z; + + if ( moveSpeed != 0.0f ) + { + CVector dist = pane->GetPosition() - point; + dist.Normalise(); + + pane->m_vecMoveSpeed += moveSpeed * dist; + } + + pane->m_vecTurn.x = float((CGeneral::GetRandomNumber() & 127) - 64) * 0.002f; + pane->m_vecTurn.y = float((CGeneral::GetRandomNumber() & 127) - 64) * 0.002f; + pane->m_vecTurn.z = float((CGeneral::GetRandomNumber() & 127) - 64) * 0.002f; + + switch ( type ) + { + case 0: + pane->m_nTimer = CTimer::GetTimeInMilliseconds(); + break; + case 1: + float dist = (pane->GetPosition() - point).Magnitude(); + pane->m_nTimer = uint32(dist*100 + CTimer::GetTimeInMilliseconds()); + break; + } + + pane->m_fGroundZ = groundZ; + pane->m_bShattered = cracked; + pane->m_fStep = upLen / float(ysteps); + pane->m_bActive = true; + } + } + } + } +} + +void +CGlass::AskForObjectToBeRenderedInGlass(CEntity *entity) +{ +#ifdef FIX_BUGS + if ( NumGlassEntities < NUM_GLASSPANES ) +#else + if ( NumGlassEntities < NUM_GLASSPANES-1 ) +#endif + { + apEntitiesToBeRendered[NumGlassEntities++] = entity; + } +} + +void +CGlass::RenderEntityInGlass(CEntity *entity) +{ + CObject *object = (CObject *)entity; + + if ( object->bGlassBroken ) + return; + + float distToCamera = (TheCamera.GetPosition() - object->GetPosition()).Magnitude(); + + if ( distToCamera > 40.0f ) + return; + + CVector fwdNorm = object->GetForward(); + fwdNorm.Normalise(); + uint8 alpha = CalcAlphaWithNormal(&fwdNorm); + + CColModel *col = object->GetColModel(); + + if ( col->numTriangles >= 2 ) + { + CVector a = object->GetMatrix() * col->vertices[0]; + CVector b = object->GetMatrix() * col->vertices[1]; + CVector c = object->GetMatrix() * col->vertices[2]; + CVector d = object->GetMatrix() * col->vertices[3]; + + if ( object->bGlassCracked ) + { + uint8 color = 255; + if ( distToCamera > 30.0f ) + color = int32((1.0f - (distToCamera - 30.0f) * 4.0f / 40.0f) * 255); + + if ( TempBufferIndicesStoredShattered >= TEMPBUFFERINDEXSHATTEREDSIZE-13 || TempBufferVerticesStoredShattered >= TEMPBUFFERVERTSHATTEREDSIZE-5 ) + RenderShatteredPolys(); + + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 3], color, color, color, color); + + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], 0.0f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], 0.0f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], 16.0f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], 0.0f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], 0.0f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], 16.0f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 3], 16.0f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 3], 16.0f); + + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 0], a.x, a.y, a.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 1], b.x, b.y, b.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 2], c.x, c.y, c.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredShattered + 3], d.x, d.y, d.z); + + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 0] = col->triangles[0].a + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 1] = col->triangles[0].b + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 2] = col->triangles[0].c + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 3] = col->triangles[1].a + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 4] = col->triangles[1].b + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 5] = col->triangles[1].c + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 6] = col->triangles[0].a + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 7] = col->triangles[0].c + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 8] = col->triangles[0].b + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 9] = col->triangles[1].a + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 10] = col->triangles[1].c + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredShattered + 11] = col->triangles[1].b + TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET; + + TempBufferIndicesStoredShattered += 12; + TempBufferVerticesStoredShattered += 4; + } + + if ( TempBufferIndicesStoredReflection >= TEMPBUFFERINDEXREFLECTIONSIZE-13 || TempBufferVerticesStoredReflection >= TEMPBUFFERVERTREFLECTIONSIZE-5 ) + RenderReflectionPolys(); + + uint8 color = 100; + if ( distToCamera > 30.0f ) + color = int32((1.0f - (distToCamera - 30.0f) * 4.0f / 40.0f) * 100); + + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 0], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 1], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 2], color, color, color, color); + RwIm3DVertexSetRGBA (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 3], color, color, color, color); + + float FwdAngle = CGeneral::GetATanOfXY(TheCamera.GetForward().x, TheCamera.GetForward().y); + float v = 2.0f * TheCamera.GetForward().z * 0.2f; + float u = float(object->m_randomSeed & 15) * 0.02f + (FwdAngle / TWOPI); + + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 0], u); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 0], v); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 1], u+0.2f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 1], v); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 2], u); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 2], v+0.2f); + RwIm3DVertexSetU (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 3], u+0.2f); + RwIm3DVertexSetV (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 3], v+0.2f); + + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 0], a.x, a.y, a.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 1], b.x, b.y, b.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 2], c.x, c.y, c.z); + RwIm3DVertexSetPos (&TempBufferRenderVertices[TempBufferVerticesStoredReflection + 3], d.x, d.y, d.z); + + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 0] = col->triangles[0].a + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 1] = col->triangles[0].b + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 2] = col->triangles[0].c + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 3] = col->triangles[1].a + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 4] = col->triangles[1].b + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 5] = col->triangles[1].c + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 6] = col->triangles[0].a + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 7] = col->triangles[0].c + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 8] = col->triangles[0].b + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 9] = col->triangles[1].a + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 10] = col->triangles[1].c + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + TempBufferRenderIndexList[TempBufferIndicesStoredReflection + 11] = col->triangles[1].b + TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET; + + TempBufferIndicesStoredReflection += 12; + TempBufferVerticesStoredReflection += 4; + } +} + +int32 +CGlass::CalcAlphaWithNormal(CVector *normal) +{ + float fwdDir = 2.0f * DotProduct(*normal, TheCamera.GetForward()); + float fwdDot = DotProduct(TheCamera.GetForward()-fwdDir*(*normal), CVector(0.57f, 0.57f, -0.57f)); + return int32(lerp(fwdDot*fwdDot*fwdDot*fwdDot*fwdDot*fwdDot, 20.0f, 255.0f)); +} + +void +CGlass::RenderHiLightPolys(void) +{ + if ( TempBufferVerticesStoredHiLight != TEMPBUFFERVERTHILIGHTOFFSET ) + { + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDONE); + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)RwTextureGetRaster(gpShadowExplosionTex)); + + LittleTest(); + + if ( RwIm3DTransform(TempBufferRenderVertices, TempBufferVerticesStoredHiLight, NULL, rwIM3D_VERTEXUV) ) + { + RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, TempBufferRenderIndexList, TempBufferIndicesStoredHiLight); + RwIm3DEnd(); + } + + TempBufferVerticesStoredHiLight = TEMPBUFFERVERTHILIGHTOFFSET; + TempBufferIndicesStoredHiLight = TEMPBUFFERINDEXHILIGHTOFFSET; + } +} + +void +CGlass::RenderShatteredPolys(void) +{ + if ( TempBufferVerticesStoredShattered != TEMPBUFFERVERTSHATTEREDOFFSET ) + { + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)RwTextureGetRaster(gpCrackedGlassTex)); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDSRCALPHA); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDINVSRCALPHA); + + LittleTest(); + + if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTSHATTEREDOFFSET], TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET, NULL, rwIM3D_VERTEXUV) ) + { + RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, &TempBufferRenderIndexList[TEMPBUFFERINDEXSHATTEREDOFFSET], TempBufferIndicesStoredShattered - TEMPBUFFERINDEXSHATTEREDOFFSET); + RwIm3DEnd(); + } + + TempBufferIndicesStoredShattered = TEMPBUFFERINDEXSHATTEREDOFFSET; + TempBufferVerticesStoredShattered = TEMPBUFFERVERTSHATTEREDOFFSET; + } +} + +void +CGlass::RenderReflectionPolys(void) +{ + if ( TempBufferVerticesStoredReflection != TEMPBUFFERVERTREFLECTIONOFFSET ) + { + RwRenderStateSet(rwRENDERSTATETEXTURERASTER, (void *)RwTextureGetRaster(gpShadowHeadLightsTex)); + RwRenderStateSet(rwRENDERSTATESRCBLEND, (void *)rwBLENDSRCALPHA); + RwRenderStateSet(rwRENDERSTATEDESTBLEND, (void *)rwBLENDINVSRCALPHA); + + LittleTest(); + + if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTREFLECTIONOFFSET], TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET, NULL, rwIM3D_VERTEXUV) ) + { + RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, &TempBufferRenderIndexList[TEMPBUFFERINDEXREFLECTIONOFFSET], TempBufferIndicesStoredReflection - TEMPBUFFERINDEXREFLECTIONOFFSET); + RwIm3DEnd(); + } + + TempBufferIndicesStoredReflection = TEMPBUFFERINDEXREFLECTIONOFFSET; + TempBufferVerticesStoredReflection = TEMPBUFFERVERTREFLECTIONOFFSET; + } +} + +void +CGlass::WindowRespondsToCollision(CEntity *entity, float amount, CVector speed, CVector point, bool explosion) +{ + CObject *object = (CObject *)entity; + + if ( object->bGlassBroken ) + return; + + object->bGlassCracked = true; + + CColModel *col = object->GetColModel(); + + CVector a = object->GetMatrix() * col->vertices[0]; + CVector b = object->GetMatrix() * col->vertices[1]; + CVector c = object->GetMatrix() * col->vertices[2]; + CVector d = object->GetMatrix() * col->vertices[3]; + + float minx = min(min(a.x, b.x), min(c.x, d.x)); + float maxx = max(max(a.x, b.x), max(c.x, d.x)); + float miny = min(min(a.y, b.y), min(c.y, d.y)); + float maxy = max(max(a.y, b.y), max(c.y, d.y)); + float minz = min(min(a.z, b.z), min(c.z, d.z)); + float maxz = max(max(a.z, b.z), max(c.z, d.z)); + + + if ( amount > 300.0f ) + { + PlayOneShotScriptObject(_SCRSOUND_GLASS_SMASH_1, object->GetPosition()); + + GeneratePanesForWindow(0, + CVector(minx, miny, minz), + CVector(0.0f, 0.0f, maxz-minz), + CVector(maxx-minx, maxy-miny, 0.0f), + speed, point, 0.1f, !!object->bGlassCracked, explosion); + } + else + { + PlayOneShotScriptObject(_SCRSOUND_GLASS_SMASH_2, object->GetPosition()); + + GeneratePanesForWindow(1, + CVector(minx, miny, minz), + CVector(0.0f, 0.0f, maxz-minz), + CVector(maxx-minx, maxy-miny, 0.0f), + speed, point, 0.1f, !!object->bGlassCracked, explosion); + } + + object->bGlassBroken = true; + object->GetPosition().z = -100.0f; +} + +void +CGlass::WindowRespondsToSoftCollision(CEntity *entity, float amount) +{ + CObject *object = (CObject *)entity; + + if ( amount > 50.0f && !object->bGlassCracked ) + { + PlayOneShotScriptObject(_SCRSOUND_GLASS_CRACK, object->GetPosition()); + object->bGlassCracked = true; + } +} + +void +CGlass::WasGlassHitByBullet(CEntity *entity, CVector point) +{ + CObject *object = (CObject *)entity; + + if ( IsGlass(object->GetModelIndex()) ) + { + if ( !object->bGlassCracked ) + { + PlayOneShotScriptObject(_SCRSOUND_GLASS_CRACK, object->GetPosition()); + object->bGlassCracked = true; + } + else + { + if ( (CGeneral::GetRandomNumber() & 3) == 2 ) + WindowRespondsToCollision(object, 0.0f, CVector(0.0f, 0.0f, 0.0f), point, false); + } + } +} + +void +CGlass::WindowRespondsToExplosion(CEntity *entity, CVector point) +{ + CObject *object = (CObject *)entity; + + CVector distToGlass = object->GetPosition() - point; + + float fDistToGlass = distToGlass.Magnitude(); + + if ( fDistToGlass < 10.0f ) + { + distToGlass.Normalise(0.3f); + WindowRespondsToCollision(object, 10000.0f, distToGlass, object->GetPosition(), true); + } + else + { + if ( fDistToGlass < 30.0f ) + object->bGlassCracked = true; + } +} + +STARTPATCHES + InjectHook(0x501F20, CGlass::Init, PATCH_JUMP); + InjectHook(0x502050, CGlass::Update, PATCH_JUMP); + InjectHook(0x502080, &CFallingGlassPane::Update, PATCH_JUMP); + InjectHook(0x502350, CGlass::Render, PATCH_JUMP); + InjectHook(0x502490, CGlass::FindFreePane, PATCH_JUMP); + InjectHook(0x5024C0, &CFallingGlassPane::Render, PATCH_JUMP); + InjectHook(0x502AC0, CGlass::GeneratePanesForWindow, PATCH_JUMP); + InjectHook(0x5033F0, CGlass::AskForObjectToBeRenderedInGlass, PATCH_JUMP); + InjectHook(0x503420, CGlass::RenderEntityInGlass, PATCH_JUMP); + InjectHook(0x503C90, CGlass::CalcAlphaWithNormal, PATCH_JUMP); + InjectHook(0x503D60, CGlass::RenderHiLightPolys, PATCH_JUMP); + InjectHook(0x503DE0, CGlass::RenderShatteredPolys, PATCH_JUMP); + InjectHook(0x503E70, CGlass::RenderReflectionPolys, PATCH_JUMP); + InjectHook(0x503F10, CGlass::WindowRespondsToCollision, PATCH_JUMP); + InjectHook(0x504630, CGlass::WindowRespondsToSoftCollision, PATCH_JUMP); + InjectHook(0x504670, CGlass::WasGlassHitByBullet, PATCH_JUMP); + InjectHook(0x504790, CGlass::WindowRespondsToExplosion, PATCH_JUMP); + //InjectHook(0x504880, `global constructor keyed to'glass.cpp, PATCH_JUMP); + //InjectHook(0x5048D0, CFallingGlassPane::~CFallingGlassPane, PATCH_JUMP); + //InjectHook(0x5048E0, CFallingGlassPane::CFallingGlassPane, PATCH_JUMP); +ENDPATCHES \ No newline at end of file diff --git a/src/render/Glass.h b/src/render/Glass.h index ad4d50f2..dccd9d3d 100644 --- a/src/render/Glass.h +++ b/src/render/Glass.h @@ -2,13 +2,52 @@ class CEntity; -class CGlass +class CFallingGlassPane : public CMatrix { public: - static void AskForObjectToBeRenderedInGlass(CEntity *ent); - static void WindowRespondsToCollision(CEntity *ent, float amount, CVector speed, CVector point, bool foo); - static void WindowRespondsToSoftCollision(CEntity *ent, float amount); - static void Render(void); - static void Update(void); - static void Init(void); + CVector m_vecMoveSpeed; + CVector m_vecTurn; + uint32 m_nTimer; + float m_fGroundZ; + float m_fStep; + uint8 m_nTriIndex; + bool m_bActive; + bool m_bShattered; + char _pad0[1]; + + CFallingGlassPane() { } + ~CFallingGlassPane() { } + + void Update(void); + void Render(void); }; + +VALIDATE_SIZE(CFallingGlassPane, 0x70); + +enum +{ + NUM_GLASSTRIANGLES = 5, +}; + +class CGlass +{ + static uint32 NumGlassEntities; + static CEntity *apEntitiesToBeRendered[NUM_GLASSENTITIES]; + static CFallingGlassPane aGlassPanes[NUM_GLASSPANES]; +public: + static void Init(void); + static void Update(void); + static void Render(void); + static CFallingGlassPane *FindFreePane(void); + static void GeneratePanesForWindow(uint32 type, CVector pos, CVector up, CVector right, CVector speed, CVector point, float moveSpeed, bool cracked, bool explosion); + static void AskForObjectToBeRenderedInGlass(CEntity *entity); + static void RenderEntityInGlass(CEntity *entity); + static int32 CalcAlphaWithNormal(CVector *normal); + static void RenderHiLightPolys(void); + static void RenderShatteredPolys(void); + static void RenderReflectionPolys(void); + static void WindowRespondsToCollision(CEntity *entity, float amount, CVector speed, CVector point, bool explosion); + static void WindowRespondsToSoftCollision(CEntity *entity, float amount); + static void WasGlassHitByBullet(CEntity *entity, CVector point); + static void WindowRespondsToExplosion(CEntity *entity, CVector point); +}; \ No newline at end of file diff --git a/src/render/Shadows.h b/src/render/Shadows.h index 66df0273..982cc463 100644 --- a/src/render/Shadows.h +++ b/src/render/Shadows.h @@ -182,3 +182,4 @@ extern RwTexture *&gpGoalTex; extern RwTexture *&gpOutline1Tex; extern RwTexture *&gpOutline2Tex; extern RwTexture *&gpOutline3Tex; +extern RwTexture *&gpCrackedGlassTex; From c394bd2a6e7cd12f40906543cf3e60cf2283145e Mon Sep 17 00:00:00 2001 From: Fire_Head Date: Thu, 2 Apr 2020 05:37:22 +0300 Subject: [PATCH 65/70] Glass cosmetic fixes --- src/render/Glass.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/render/Glass.cpp b/src/render/Glass.cpp index b082a9e1..41d31985 100644 --- a/src/render/Glass.cpp +++ b/src/render/Glass.cpp @@ -21,7 +21,7 @@ CFallingGlassPane CGlass::aGlassPanes[NUM_GLASSPANES]; CVector2D CentersWithTriangle[NUM_GLASSTRIANGLES]; -CVector2D CoorsWithTriangle[NUM_GLASSTRIANGLES][3] = +const CVector2D CoorsWithTriangle[NUM_GLASSTRIANGLES][3] = { { CVector2D(0.0f, 0.0f), @@ -116,7 +116,7 @@ CFallingGlassPane::Update(void) CParticle::AddParticle(PARTICLE_CAR_DEBRIS, pos, dir, - NULL, + nil, CGeneral::GetRandomNumberInRange(0.02f, 0.2f), color, CGeneral::GetRandomNumberInRange(-40, 40), @@ -287,7 +287,7 @@ CGlass::FindFreePane(void) return &aGlassPanes[i]; } - return NULL; + return nil; } void @@ -539,7 +539,7 @@ CGlass::RenderHiLightPolys(void) LittleTest(); - if ( RwIm3DTransform(TempBufferRenderVertices, TempBufferVerticesStoredHiLight, NULL, rwIM3D_VERTEXUV) ) + if ( RwIm3DTransform(TempBufferRenderVertices, TempBufferVerticesStoredHiLight, nil, rwIM3D_VERTEXUV) ) { RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, TempBufferRenderIndexList, TempBufferIndicesStoredHiLight); RwIm3DEnd(); @@ -561,7 +561,7 @@ CGlass::RenderShatteredPolys(void) LittleTest(); - if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTSHATTEREDOFFSET], TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET, NULL, rwIM3D_VERTEXUV) ) + if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTSHATTEREDOFFSET], TempBufferVerticesStoredShattered - TEMPBUFFERVERTSHATTEREDOFFSET, nil, rwIM3D_VERTEXUV) ) { RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, &TempBufferRenderIndexList[TEMPBUFFERINDEXSHATTEREDOFFSET], TempBufferIndicesStoredShattered - TEMPBUFFERINDEXSHATTEREDOFFSET); RwIm3DEnd(); @@ -583,7 +583,7 @@ CGlass::RenderReflectionPolys(void) LittleTest(); - if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTREFLECTIONOFFSET], TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET, NULL, rwIM3D_VERTEXUV) ) + if ( RwIm3DTransform(&TempBufferRenderVertices[TEMPBUFFERVERTREFLECTIONOFFSET], TempBufferVerticesStoredReflection - TEMPBUFFERVERTREFLECTIONOFFSET, nil, rwIM3D_VERTEXUV) ) { RwIm3DRenderIndexedPrimitive(rwPRIMTYPETRILIST, &TempBufferRenderIndexList[TEMPBUFFERINDEXREFLECTIONOFFSET], TempBufferIndicesStoredReflection - TEMPBUFFERINDEXREFLECTIONOFFSET); RwIm3DEnd(); @@ -718,4 +718,4 @@ STARTPATCHES //InjectHook(0x504880, `global constructor keyed to'glass.cpp, PATCH_JUMP); //InjectHook(0x5048D0, CFallingGlassPane::~CFallingGlassPane, PATCH_JUMP); //InjectHook(0x5048E0, CFallingGlassPane::CFallingGlassPane, PATCH_JUMP); -ENDPATCHES \ No newline at end of file +ENDPATCHES From 6e6912b6137d6f1fdff6410f0369266cd7d6fb30 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Thu, 2 Apr 2020 09:19:33 +0300 Subject: [PATCH 66/70] fixes --- src/audio/MusicManager.cpp | 4 ++-- src/core/Radar.cpp | 5 ++++- src/render/Hud.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/audio/MusicManager.cpp b/src/audio/MusicManager.cpp index 1f1c343a..d840c57b 100644 --- a/src/audio/MusicManager.cpp +++ b/src/audio/MusicManager.cpp @@ -17,8 +17,6 @@ cMusicManager &MusicManager = *(cMusicManager *)0x8F3964; int32 &gNumRetunePresses = *(int32 *)0x650B80; -wchar *pCurrentStation = (wchar *)0x650B9C; -uint8 &cDisplay = *(uint8 *)0x650BA1; int32 &gRetuneCounter = *(int32*)0x650B84; bool& bHasStarted = *(bool*)0x650B7C; @@ -72,6 +70,8 @@ cMusicManager::DisplayRadioStationName() int8 pRetune; int8 gStreamedSound; int8 gRetuneCounter; + static wchar *pCurrentStation = nil; + static uint8 cDisplay = 0; if(!CTimer::GetIsPaused() && !TheCamera.m_WideScreenOn && PlayerInCar() && !CReplay::IsPlayingBack()) { diff --git a/src/core/Radar.cpp b/src/core/Radar.cpp index 1c634760..f1d8ec96 100644 --- a/src/core/Radar.cpp +++ b/src/core/Radar.cpp @@ -1106,8 +1106,11 @@ void CRadar::TransformRadarPointToRealWorldSpace(CVector2D &out, const CVector2D // Radar space goes from -1.0 to 1.0 in x and y, top right is (1.0, 1.0) void CRadar::TransformRadarPointToScreenSpace(CVector2D &out, const CVector2D &in) { - // FIX? scale RADAR_LEFT here somehow +#ifdef FIX_BUGS + out.x = (in.x + 1.0f)*0.5f*SCREEN_SCALE_X(RADAR_WIDTH) + SCREEN_SCALE_X(RADAR_LEFT); +#else out.x = (in.x + 1.0f)*0.5f*SCREEN_SCALE_X(RADAR_WIDTH) + RADAR_LEFT; +#endif out.y = (1.0f - in.y)*0.5f*SCREEN_SCALE_Y(RADAR_HEIGHT) + SCREEN_SCALE_FROM_BOTTOM(RADAR_BOTTOM + RADAR_HEIGHT); } diff --git a/src/render/Hud.cpp b/src/render/Hud.cpp index 2f523e17..c4aca8e4 100644 --- a/src/render/Hud.cpp +++ b/src/render/Hud.cpp @@ -793,8 +793,11 @@ void CHud::Draw() if (m_ItemToFlash == ITEM_RADAR && CTimer::GetFrameCounter() & 8 || m_ItemToFlash != ITEM_RADAR) { CRadar::DrawMap(); CRect rect(0.0f, 0.0f, SCREEN_SCALE_X(RADAR_WIDTH), SCREEN_SCALE_Y(RADAR_HEIGHT)); - // FIX? scale RADAR_LEFT here somehow +#ifdef FIX_BUGS + rect.Translate(SCREEN_SCALE_X(RADAR_LEFT), SCREEN_SCALE_FROM_BOTTOM(RADAR_BOTTOM + RADAR_HEIGHT)); +#else rect.Translate(RADAR_LEFT, SCREEN_SCALE_FROM_BOTTOM(RADAR_BOTTOM + RADAR_HEIGHT)); +#endif rect.Grow(4.0f); Sprites[HUD_RADARDISC].Draw(rect, CRGBA(0, 0, 0, 255)); CRadar::DrawBlips(); @@ -1094,7 +1097,6 @@ void CHud::DrawAfterFade() CFont::SetColor(CRGBA(175, 175, 175, 255)); CFont::PrintString(SCREEN_SCALE_X(26.0f), SCREEN_SCALE_Y(28.0f + (150.0f - PagerXOffset) * 0.6f), CHud::m_HelpMessageToPrint); CFont::SetAlphaFade(255.0f); - CFont::DrawFonts(); } } else From 92ec403191af3989e0c480e860315cf6813f1202 Mon Sep 17 00:00:00 2001 From: Sergeanur Date: Thu, 2 Apr 2020 09:51:35 +0300 Subject: [PATCH 67/70] Fix CFont type uint16 -> wchar --- src/render/Font.cpp | 40 ++++++++++++++++++++-------------------- src/render/Font.h | 22 +++++++++++----------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/render/Font.cpp b/src/render/Font.cpp index 0d79eee3..6f336f1e 100644 --- a/src/render/Font.cpp +++ b/src/render/Font.cpp @@ -116,7 +116,7 @@ int16 CFont::Size[3][193] = { #endif }; -uint16 foreign_table[128] = { +wchar foreign_table[128] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -228,7 +228,7 @@ CFont::InitPerFrame(void) } void -CFont::PrintChar(float x, float y, uint16 c) +CFont::PrintChar(float x, float y, wchar c) { if(x <= 0.0f || x > SCREEN_WIDTH || y <= 0.0f || y > SCREEN_HEIGHT) // BUG: game uses SCREENW again @@ -274,14 +274,14 @@ CFont::PrintChar(float x, float y, uint16 c) } void -CFont::PrintString(float xstart, float ystart, uint16 *s) +CFont::PrintString(float xstart, float ystart, wchar *s) { CRect rect; int numSpaces; float lineLength; float x, y; bool first; - uint16 *start, *t; + wchar *start, *t; if(*s == '*') return; @@ -357,11 +357,11 @@ CFont::PrintString(float xstart, float ystart, uint16 *s) } int -CFont::GetNumberLines(float xstart, float ystart, uint16 *s) +CFont::GetNumberLines(float xstart, float ystart, wchar *s) { int n; float x, y; - uint16 *t; + wchar *t; n = 0; if(Details.centre || Details.rightJustify) @@ -400,12 +400,12 @@ CFont::GetNumberLines(float xstart, float ystart, uint16 *s) } void -CFont::GetTextRect(CRect *rect, float xstart, float ystart, uint16 *s) +CFont::GetTextRect(CRect *rect, float xstart, float ystart, wchar *s) { int numLines; float x, y; int16 maxlength; - uint16 *t; + wchar *t; maxlength = 0; numLines = 0; @@ -469,9 +469,9 @@ CFont::GetTextRect(CRect *rect, float xstart, float ystart, uint16 *s) } void -CFont::PrintString(float x, float y, uint16 *start, uint16 *end, float spwidth) +CFont::PrintString(float x, float y, wchar *start, wchar *end, float spwidth) { - uint16 *s, c, unused; + wchar *s, c, unused; for(s = start; s < end; s++){ if(*s == '~') @@ -487,7 +487,7 @@ CFont::PrintString(float x, float y, uint16 *start, uint16 *end, float spwidth) } float -CFont::GetCharacterWidth(uint16 c) +CFont::GetCharacterWidth(wchar c) { #ifdef MORE_LANGUAGES if (Details.proportional) @@ -503,7 +503,7 @@ CFont::GetCharacterWidth(uint16 c) } float -CFont::GetCharacterSize(uint16 c) +CFont::GetCharacterSize(wchar c) { #ifdef MORE_LANGUAGES if(Details.proportional) @@ -519,7 +519,7 @@ CFont::GetCharacterSize(uint16 c) } float -CFont::GetStringWidth(uint16 *s, bool spaces) +CFont::GetStringWidth(wchar *s, bool spaces) { float w; @@ -537,8 +537,8 @@ CFont::GetStringWidth(uint16 *s, bool spaces) return w; } -uint16* -CFont::GetNextSpace(uint16 *s) +wchar* +CFont::GetNextSpace(wchar *s) { for(; *s != ' ' && *s != '\0'; s++) if(*s == '~'){ @@ -551,8 +551,8 @@ CFont::GetNextSpace(uint16 *s) return s; } -uint16* -CFont::ParseToken(uint16 *s, uint16*) +wchar* +CFont::ParseToken(wchar *s, wchar*) { s++; if(Details.color.r || Details.color.g || Details.color.b) @@ -582,7 +582,7 @@ CFont::DrawFonts(void) CSprite2d::DrawBank(Details.bank+2); } -uint16 +wchar CFont::character_code(uint8 c) { if(c < 128) @@ -596,10 +596,10 @@ STARTPATCHES InjectHook(0x500BA0, CFont::Shutdown, PATCH_JUMP); InjectHook(0x500BE0, CFont::InitPerFrame, PATCH_JUMP); InjectHook(0x500C30, CFont::PrintChar, PATCH_JUMP); - InjectHook(0x500F50, (void (*)(float, float, uint16*))CFont::PrintString, PATCH_JUMP); + InjectHook(0x500F50, (void (*)(float, float, wchar*))CFont::PrintString, PATCH_JUMP); InjectHook(0x501260, CFont::GetNumberLines, PATCH_JUMP); InjectHook(0x5013B0, CFont::GetTextRect, PATCH_JUMP); - InjectHook(0x501730, (void (*)(float, float, uint16*, uint16*, float))CFont::PrintString, PATCH_JUMP); + InjectHook(0x501730, (void (*)(float, float, wchar*, wchar*, float))CFont::PrintString, PATCH_JUMP); InjectHook(0x5017E0, CFont::GetCharacterWidth, PATCH_JUMP); InjectHook(0x501840, CFont::GetCharacterSize, PATCH_JUMP); InjectHook(0x5018A0, CFont::GetStringWidth, PATCH_JUMP); diff --git a/src/render/Font.h b/src/render/Font.h index 26309377..0659dda1 100644 --- a/src/render/Font.h +++ b/src/render/Font.h @@ -64,18 +64,18 @@ public: static void Initialise(void); static void Shutdown(void); static void InitPerFrame(void); - static void PrintChar(float x, float y, uint16 c); - static void PrintString(float x, float y, uint16 *s); - static int GetNumberLines(float xstart, float ystart, uint16 *s); - static void GetTextRect(CRect *rect, float xstart, float ystart, uint16 *s); - static void PrintString(float x, float y, uint16 *start, uint16 *end, float spwidth); - static float GetCharacterWidth(uint16 c); - static float GetCharacterSize(uint16 c); - static float GetStringWidth(uint16 *s, bool spaces = false); - static uint16 *GetNextSpace(uint16 *s); - static uint16 *ParseToken(uint16 *s, uint16*); + static void PrintChar(float x, float y, wchar c); + static void PrintString(float x, float y, wchar *s); + static int GetNumberLines(float xstart, float ystart, wchar *s); + static void GetTextRect(CRect *rect, float xstart, float ystart, wchar *s); + static void PrintString(float x, float y, wchar *start, wchar *end, float spwidth); + static float GetCharacterWidth(wchar c); + static float GetCharacterSize(wchar c); + static float GetStringWidth(wchar *s, bool spaces = false); + static wchar *GetNextSpace(wchar *s); + static wchar *ParseToken(wchar *s, wchar*); static void DrawFonts(void); - static uint16 character_code(uint8 c); + static wchar character_code(uint8 c); static CFontDetails GetDetails() { return Details; } static void SetScale(float x, float y) { Details.scaleX = x; Details.scaleY = y; } From 7ff5a3a65c3106cb488a5b0a4f25d0b5450d489f Mon Sep 17 00:00:00 2001 From: aap Date: Thu, 2 Apr 2020 12:48:01 +0200 Subject: [PATCH 68/70] CCamera fixes --- README.md | 1 - src/control/Garages.h | 2 + src/control/Remote.cpp | 2 +- src/control/SceneEdit.cpp | 1 + src/control/SceneEdit.h | 1 + src/control/Script.cpp | 8 +- src/core/AnimViewer.cpp | 2 +- src/core/Cam.cpp | 123 +- src/core/Camera.cpp | 3556 ++++++++++++++++++++++++++++++++--- src/core/Camera.h | 244 ++- src/core/CutsceneMgr.h | 1 + src/core/Frontend.cpp | 8 +- src/core/Frontend.h | 2 +- src/core/Pad.h | 2 +- src/core/config.h | 2 +- src/core/re3.cpp | 6 +- src/peds/Ped.cpp | 8 +- src/peds/PlayerPed.cpp | 16 +- src/vehicles/Automobile.cpp | 6 +- 19 files changed, 3544 insertions(+), 447 deletions(-) diff --git a/README.md b/README.md index 403db674..3f98c9a5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ cAudioManager - WIP CBoat CBrightLights CBulletInfo -CCamera CCrane CCranes CCullZone diff --git a/src/control/Garages.h b/src/control/Garages.h index 3f471555..e3864a48 100644 --- a/src/control/Garages.h +++ b/src/control/Garages.h @@ -5,6 +5,7 @@ #include "config.h" class CVehicle; +class CCamera; enum eGarageState : int8 { @@ -168,6 +169,7 @@ class CGarage friend class CGarages; friend class cAudioManager; + friend class CCamera; }; static_assert(sizeof(CGarage) == 140, "CGarage"); diff --git a/src/control/Remote.cpp b/src/control/Remote.cpp index e3891502..f7d12702 100644 --- a/src/control/Remote.cpp +++ b/src/control/Remote.cpp @@ -35,7 +35,7 @@ CRemote::GivePlayerRemoteControlledCar(float x, float y, float z, float rot, uin CWorld::Players[CWorld::PlayerInFocus].m_pRemoteVehicle = car; CWorld::Players[CWorld::PlayerInFocus].m_pRemoteVehicle->RegisterReference((CEntity**)&CWorld::Players[CWorld::PlayerInFocus].m_pRemoteVehicle); - TheCamera.TakeControl(car, CCam::MODE_BEHINDCAR, INTERPOLATION, CAM_CONTROLLER_1); + TheCamera.TakeControl(car, CCam::MODE_BEHINDCAR, INTERPOLATION, CAMCONTROL_SCRIPT); } void diff --git a/src/control/SceneEdit.cpp b/src/control/SceneEdit.cpp index 4c05e11b..3e55d431 100644 --- a/src/control/SceneEdit.cpp +++ b/src/control/SceneEdit.cpp @@ -2,6 +2,7 @@ #include "patcher.h" #include "SceneEdit.h" +bool &CSceneEdit::m_bEditOn = *(bool*)0x95CD77; int32 &CSceneEdit::m_bCameraFollowActor = *(int*)0x940590; bool &CSceneEdit::m_bRecording = *(bool*)0x95CD1F; CVector &CSceneEdit::m_vecCurrentPosition = *(CVector*)0x943064; diff --git a/src/control/SceneEdit.h b/src/control/SceneEdit.h index ec321b27..0de72c19 100644 --- a/src/control/SceneEdit.h +++ b/src/control/SceneEdit.h @@ -3,6 +3,7 @@ class CSceneEdit { public: + static bool &m_bEditOn; static int32 &m_bCameraFollowActor; static bool &m_bRecording; static CVector &m_vecCurrentPosition; diff --git a/src/control/Script.cpp b/src/control/Script.cpp index 8a79ba1d..f96ec060 100644 --- a/src/control/Script.cpp +++ b/src/control/Script.cpp @@ -3073,7 +3073,7 @@ int8 CRunningScript::ProcessCommands300To399(int32 command) { CollectParameters(&m_nIp, 3); // ScriptParams[0] is unused. - TheCamera.TakeControl(nil, ScriptParams[1], ScriptParams[2], CAM_CONTROLLER_1); + TheCamera.TakeControl(nil, ScriptParams[1], ScriptParams[2], CAMCONTROL_SCRIPT); return 0; } case COMMAND_POINT_CAMERA_AT_CAR: @@ -3081,7 +3081,7 @@ int8 CRunningScript::ProcessCommands300To399(int32 command) CollectParameters(&m_nIp, 3); CVehicle* pVehicle = CPools::GetVehiclePool()->GetAt(ScriptParams[0]); assert(pVehicle); - TheCamera.TakeControl(pVehicle, ScriptParams[1], ScriptParams[2], CAM_CONTROLLER_1); + TheCamera.TakeControl(pVehicle, ScriptParams[1], ScriptParams[2], CAMCONTROL_SCRIPT); return 0; } case COMMAND_POINT_CAMERA_AT_CHAR: @@ -3089,7 +3089,7 @@ int8 CRunningScript::ProcessCommands300To399(int32 command) CollectParameters(&m_nIp, 3); CPed* pPed = CPools::GetPedPool()->GetAt(ScriptParams[0]); assert(pPed); - TheCamera.TakeControl(pPed, ScriptParams[1], ScriptParams[2], CAM_CONTROLLER_1); + TheCamera.TakeControl(pPed, ScriptParams[1], ScriptParams[2], CAMCONTROL_SCRIPT); return 0; } case COMMAND_RESTORE_CAMERA: @@ -3140,7 +3140,7 @@ int8 CRunningScript::ProcessCommands300To399(int32 command) CVector pos = *(CVector*)&ScriptParams[0]; if (pos.z <= MAP_Z_LOW_LIMIT) pos.z = CWorld::FindGroundZForCoord(pos.x, pos.y); - TheCamera.TakeControlNoEntity(pos, ScriptParams[3], CAM_CONTROLLER_1); + TheCamera.TakeControlNoEntity(pos, ScriptParams[3], CAMCONTROL_SCRIPT); return 0; } case COMMAND_ADD_BLIP_FOR_CAR_OLD: diff --git a/src/core/AnimViewer.cpp b/src/core/AnimViewer.cpp index 20a0098d..1086db20 100644 --- a/src/core/AnimViewer.cpp +++ b/src/core/AnimViewer.cpp @@ -294,7 +294,7 @@ CAnimViewer::Update(void) } newEntity->GetPosition() = CVector(0.0f, 0.0f, 0.0f); CWorld::Add(newEntity); - TheCamera.TakeControl(pTarget, CCam::MODE_MODELVIEW, JUMP_CUT, CAM_CONTROLLER_1); + TheCamera.TakeControl(pTarget, CCam::MODE_MODELVIEW, JUMP_CUT, CAMCONTROL_SCRIPT); } if (pTarget->m_type == ENTITY_TYPE_VEHICLE || pTarget->m_type == ENTITY_TYPE_PED || pTarget->m_type == ENTITY_TYPE_OBJECT) { ((CPhysical*)pTarget)->m_vecMoveSpeed = CVector(0.0f, 0.0f, 0.0f); diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index dd1b5ce2..b9e8e94e 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -31,8 +31,7 @@ bool PrintDebugCode = false; int16 &DebugCamMode = *(int16*)0x95CCF2; #ifdef FREE_CAM -bool bFreePadCam = false; -bool bFreeMouseCam = false; +bool CCamera::bFreeCam = false; int nPreviousMode = -1; #endif @@ -146,7 +145,7 @@ CCam::Process(void) Process_FollowPedWithMouse(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #ifdef FREE_CAM - if(bFreePadCam) + if(CCamera::bFreeCam) Process_FollowPed_Rotation(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #endif @@ -187,7 +186,7 @@ CCam::Process(void) break; case MODE_CAM_ON_A_STRING: #ifdef FREE_CAM - if(bFreeMouseCam || bFreePadCam) + if(CCamera::bFreeCam) Process_FollowCar_SA(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #endif @@ -204,7 +203,7 @@ CCam::Process(void) break; case MODE_BEHINDBOAT: #ifdef FREE_CAM - if (bFreeMouseCam || bFreePadCam) + if (CCamera::bFreeCam) Process_FollowCar_SA(CameraTarget, TargetOrientation, SpeedVar, TargetSpeedVar); else #endif @@ -267,7 +266,7 @@ CCam::Process(void) float DistOnGround = TargetToCam.Magnitude2D(); m_fTrueBeta = CGeneral::GetATanOfXY(TargetToCam.x, TargetToCam.y); m_fTrueAlpha = CGeneral::GetATanOfXY(TargetToCam.z, DistOnGround); - if(TheCamera.m_uiTransitionState == 0) // TODO? what values are possible? enum? + if(TheCamera.m_uiTransitionState == 0) KeepTrackOfTheSpeed(Source, m_cvecTargetCoorsForFudgeInter, Up, m_fTrueAlpha, m_fTrueBeta, FOV); // Look Behind, Left, Right @@ -421,11 +420,11 @@ CCam::ProcessSpecialHeightRoutines(void) float DistScale = (2.1f - dist)/2.1f; if(Mode == MODE_FOLLOWPED){ - if(TheCamera.PedZoomIndicator == 1.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_1) Offset = 0.45*DistScale + PedZDist; - if(TheCamera.PedZoomIndicator == 2.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_2) Offset = 0.35*DistScale + PedZDist; - if(TheCamera.PedZoomIndicator == 3.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_3) Offset = 0.25*DistScale + PedZDist; if(Abs(CGeneral::GetRadianAngleBetweenPoints(CamToPed.x, CamToPed.y, CamToTarget.x, CamToTarget.y)) > HALFPI) Offset += 0.3f; @@ -575,11 +574,11 @@ CCam::ProcessSpecialHeightRoutines(void) m_fRoadOffSet = 1.4f; }else{ if(Mode == MODE_FOLLOWPED){ - if(TheCamera.PedZoomIndicator == 1.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_1) m_fRoadOffSet += 0.2f; - if(TheCamera.PedZoomIndicator == 2.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_2) m_fRoadOffSet += 0.5f; - if(TheCamera.PedZoomIndicator == 3.0f) + if(TheCamera.PedZoomIndicator == CAM_ZOOM_3) m_fRoadOffSet += 0.95f; } } @@ -636,7 +635,7 @@ CCam::LookBehind(void) Source.y = Dist*Sin(TargetOrientation) + TargetCoors.y; Source.z -= 1.0f; if(CWorld::ProcessLineOfSight(TargetCoors, Source, colPoint, entity, true, false, false, true, false, true, true)){ - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); Source = colPoint.point; } Source.z += 1.0f; @@ -800,7 +799,7 @@ CCam::ClipIfPedInFrontOfPlayer(void) if(Abs(DeltaAngle) < HALFPI){ fDist = Sqrt(SQR(vDist.x) + SQR(vDist.y)); if(fDist < 1.25f){ - Near = 0.9f - (1.25f - fDist); + Near = DEFAULT_NEAR - (1.25f - fDist); if(Near < 0.05f) Near = 0.05f; RwCameraSetNearClipPlane(Scene.camera, Near); @@ -1044,7 +1043,7 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl }else{ LateralDist = 0.8f; CenterDist = 1.35f; - if(TheCamera.PedZoomIndicator == 1.0f || TheCamera.PedZoomIndicator == 4.0f){ + if(TheCamera.PedZoomIndicator == CAM_ZOOM_1 || TheCamera.PedZoomIndicator == CAM_ZOOM_TOPDOWN){ LateralDist = 1.25f; CenterDist = 1.6f; } @@ -1082,7 +1081,6 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl else IdealSource = TargetCoors + CVector(1.0f, 1.0f, 0.0f); - // TODO: what's transition beta? if(TheCamera.m_bUseTransitionBeta && ResetStatics){ CVector VecDistance; IdealSource.x = TargetCoors.x + GroundDist*Cos(m_fTransitionBeta); @@ -1111,17 +1109,17 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl // BUG? is this ever used? // The values seem to be roughly m_fPedZoomValueSmooth + 1.85 if(ResetStatics){ - if(TheCamera.PedZoomIndicator == 1.0) m_fRealGroundDist = 2.090556f; - if(TheCamera.PedZoomIndicator == 2.0) m_fRealGroundDist = 3.34973f; - if(TheCamera.PedZoomIndicator == 3.0) m_fRealGroundDist = 4.704914f; - if(TheCamera.PedZoomIndicator == 4.0) m_fRealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_1) m_fRealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_2) m_fRealGroundDist = 3.34973f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_3) m_fRealGroundDist = 4.704914f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_TOPDOWN) m_fRealGroundDist = 2.090556f; } // And what is this? It's only used for collision and rotation it seems float RealGroundDist; - if(TheCamera.PedZoomIndicator == 1.0) RealGroundDist = 2.090556f; - if(TheCamera.PedZoomIndicator == 2.0) RealGroundDist = 3.34973f; - if(TheCamera.PedZoomIndicator == 3.0) RealGroundDist = 4.704914f; - if(TheCamera.PedZoomIndicator == 4.0) RealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_1) RealGroundDist = 2.090556f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_2) RealGroundDist = 3.34973f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_3) RealGroundDist = 4.704914f; + if(TheCamera.PedZoomIndicator == CAM_ZOOM_TOPDOWN) RealGroundDist = 2.090556f; if(m_fCloseInPedHeightOffset > 0.00001f) RealGroundDist = 1.7016f; @@ -1292,8 +1290,8 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl // Now do the Beta rotation - float Distance = (IdealSource - TargetCoors).Magnitude2D(); - m_fDistanceBeforeChanges = Distance; + float RotDistance = (IdealSource - TargetCoors).Magnitude2D(); + m_fDistanceBeforeChanges = RotDistance; if(Rotating){ m_bFixingBeta = true; @@ -1334,8 +1332,8 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl BetaSpeed = 0.0f; } - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); + Source.x = TargetCoors.x + RotDistance * Cos(Beta); + Source.y = TargetCoors.y + RotDistance * Sin(Beta); // Check if we can stop rotating DeltaBeta = FixedTargetOrientation - Beta; @@ -1354,18 +1352,18 @@ CCam::Process_FollowPed(const CVector &CameraTarget, float TargetOrientation, fl HackPlayerOnStoppingTrain || Rotating){ if(TheCamera.m_bCamDirectlyBehind){ Beta = TargetOrientation + PI; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); + Source.x = TargetCoors.x + RotDistance * Cos(Beta); + Source.y = TargetCoors.y + RotDistance * Sin(Beta); } if(TheCamera.m_bCamDirectlyInFront){ Beta = TargetOrientation; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); + Source.x = TargetCoors.x + RotDistance * Cos(Beta); + Source.y = TargetCoors.y + RotDistance * Sin(Beta); } if(HackPlayerOnStoppingTrain){ Beta = TargetOrientation + PI; - Source.x = TargetCoors.x + Distance * Cos(Beta); - Source.y = TargetCoors.y + Distance * Sin(Beta); + Source.x = TargetCoors.x + RotDistance * Cos(Beta); + Source.y = TargetCoors.y + RotDistance * Sin(Beta); m_fDimensionOfHighestNearCar = 0.0f; m_fCamBufferedHeight = 0.0f; m_fCamBufferedHeightSpeed = 0.0f; @@ -1551,7 +1549,7 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient // SA code #ifdef FREE_CAM - if((bFreeMouseCam && Alpha > 0.0f) || (!bFreeMouseCam && Alpha > fBaseDist)) + if((CCamera::bFreeCam && Alpha > 0.0f) || (!CCamera::bFreeCam && Alpha > fBaseDist)) #else if(Alpha > fBaseDist) // comparing an angle against a distance? #endif @@ -1586,14 +1584,14 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient if(CWorld::ProcessLineOfSight(colPoint.point, Source, colPoint, entity, true, true, true, true, false, false, true)){ PedColDist = (TargetCoors - colPoint.point).Magnitude(); Source = colPoint.point; - if(PedColDist < 0.9f + 0.3f) + if(PedColDist < DEFAULT_NEAR + 0.3f) RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); }else{ - RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, 0.9f)); + RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, DEFAULT_NEAR)); } }else{ Source = colPoint.point; - if(PedColDist < 0.9f + 0.3f) + if(PedColDist < DEFAULT_NEAR + 0.3f) RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); } } @@ -1640,7 +1638,7 @@ CCam::Process_FollowPedWithMouse(const CVector &CameraTarget, float TargetOrient CPed *player = FindPlayerPed(); float PlayerDist = (Source - player->GetPosition()).Magnitude(); if(PlayerDist < 2.75f) - Near = PlayerDist/2.75f * 0.9f - 0.3f; + Near = PlayerDist/2.75f * DEFAULT_NEAR - 0.3f; RwCameraSetNearClipPlane(Scene.camera, max(Near, 0.1f)); } } @@ -1800,11 +1798,11 @@ CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, floa float zoomvalue = TheCamera.CarZoomValueSmooth; if(zoomvalue < 0.1f) zoomvalue = 0.1f; - if(TheCamera.CarZoomIndicator == 1.0f) + if(TheCamera.CarZoomIndicator == CAM_ZOOM_1) ModeAlpha = CGeneral::GetATanOfXY(23.0f, zoomvalue); // near - else if(TheCamera.CarZoomIndicator == 2.0f) + else if(TheCamera.CarZoomIndicator == CAM_ZOOM_2) ModeAlpha = CGeneral::GetATanOfXY(10.8f, zoomvalue); // mid - else if(TheCamera.CarZoomIndicator == 3.0f) + else if(TheCamera.CarZoomIndicator == CAM_ZOOM_3) ModeAlpha = CGeneral::GetATanOfXY(7.0f, zoomvalue); // far @@ -1900,7 +1898,7 @@ CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, floa PreviousNearCheckNearClipSmall = false; if(!CamClear){ PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); DeltaAlpha = TargetAlpha - (Alpha + ModeAlpha); while(DeltaAlpha >= PI) DeltaAlpha -= 2*PI; @@ -1918,7 +1916,7 @@ CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, floa if(CamClear) if(CamZ - CamGround2 < 1.5f){ PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); float a; if(Length == 0.0f || CamGround2 + 1.5f - TargetCoors.z == 0.0f) @@ -1934,7 +1932,7 @@ CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, floa float CamRoof2 = CWorld::FindRoofZFor3DCoord(Source.x, Source.y, CamZ, &FoundRoof); if(FoundRoof && CamZ - CamRoof2 < 1.5f){ PreviousNearCheckNearClipSmall = true; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); if(CamRoof2 > TargetCoors.z + 3.5f) CamRoof2 = TargetCoors.z + 3.5f; @@ -1956,7 +1954,7 @@ CCam::WorkOutCamHeight(const CVector &TargetCoors, float TargetOrientation, floa LastAlphaSpeedStep = AlphaSpeedStep; }else{ if(PreviousNearCheckNearClipSmall) - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); } WellBufferMe(LastTargetAlphaWithCollisionOn, &Alpha, &AlphaSpeed, LastTopAlphaSpeed, LastAlphaSpeedStep, true); @@ -3204,7 +3202,8 @@ CCam::Process_BehindBoat(const CVector &CameraTarget, float TargetOrientation, f static float WaterZAddition = 2.75f; float WaterLevel = 0.0f; float s, c; - float Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); + + Beta = CGeneral::GetATanOfXY(TargetCoors.x - Source.x, TargetCoors.y - Source.y); FOV = DefaultFOV; if(ResetStatics){ @@ -3717,7 +3716,7 @@ CCam::Process_Debug(const CVector&, float, float, float) static float PanSpeedY = 0.0f; CVector TargetCoors; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); FOV = DefaultFOV; Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; @@ -3814,7 +3813,7 @@ CCam::Process_Debug(const CVector&, float, float, float) static float Speed = 0.0f; CVector TargetCoors; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); FOV = DefaultFOV; Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; @@ -3887,7 +3886,7 @@ CCam::Process_Editor(const CVector&, float, float, float) } ResetStatics = false; - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); FOV = DefaultFOV; Alpha += DEGTORAD(CPad::GetPad(1)->GetLeftStickY()) / 50.0f; Beta += DEGTORAD(CPad::GetPad(1)->GetLeftStickX())*1.5f / 19.0f; @@ -4465,11 +4464,14 @@ CCam::Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrient float MouseX = CPad::GetPad(0)->GetMouseX(); float MouseY = CPad::GetPad(0)->GetMouseY(); float LookLeftRight, LookUpDown; - if(bFreeMouseCam && (MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ +/* + if((MouseX != 0.0f || MouseY != 0.0f) && !CPad::GetPad(0)->ArePlayerControlsDisabled()){ UseMouse = true; LookLeftRight = -2.5f*MouseX; LookUpDown = 4.0f*MouseY; - }else{ + }else +*/ + { LookLeftRight = -CPad::GetPad(0)->LookAroundLeftRight(); LookUpDown = CPad::GetPad(0)->LookAroundUpDown(); } @@ -4553,14 +4555,14 @@ CCam::Process_FollowPed_Rotation(const CVector &CameraTarget, float TargetOrient if(CWorld::ProcessLineOfSight(colPoint.point, Source, colPoint, entity, true, true, true, true, false, false, true)){ PedColDist = (TargetCoors - colPoint.point).Magnitude(); Source = colPoint.point; - if(PedColDist < 0.9f + 0.3f) + if(PedColDist < DEFAULT_NEAR + 0.3f) RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); }else{ - RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, 0.9f)); + RwCameraSetNearClipPlane(Scene.camera, min(ColCamDist-0.35f, DEFAULT_NEAR)); } }else{ Source = colPoint.point; - if(PedColDist < 0.9f + 0.3f) + if(PedColDist < DEFAULT_NEAR + 0.3f) RwCameraSetNearClipPlane(Scene.camera, max(PedColDist-0.3f, 0.05f)); } } @@ -4922,7 +4924,7 @@ CCam::Process_FollowCar_SA(const CVector& CameraTarget, float TargetOrientation, bool mouseChangesBeta = false; // FIX: Disable mouse movement in drive-by, it's buggy. Original SA bug. - if (bFreeMouseCam && CCamera::m_bUseMouse3rdPerson && !pad->ArePlayerControlsDisabled() && nextDirectionIsForward) { + if (/*bFreeMouseCam &&*/ CCamera::m_bUseMouse3rdPerson && !pad->ArePlayerControlsDisabled() && nextDirectionIsForward) { float mouseY = pad->GetMouseY() * 2.0f; float mouseX = pad->GetMouseX() * -2.0f; @@ -5093,10 +5095,10 @@ CCam::Process_FollowCar_SA(const CVector& CameraTarget, float TargetOrientation, } else { if (!CWorld::ProcessLineOfSight(foundCol.point, Source, foundCol, foundEnt, true, dontCollideWithCars < 0.1f, false, true, false, true, false)) { float lessClip = obstacleCamDist - 0.35f; - if (lessClip <= 0.9f) + if (lessClip <= DEFAULT_NEAR) RwCameraSetNearClipPlane(Scene.camera, lessClip); else - RwCameraSetNearClipPlane(Scene.camera, 0.9f); + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); } else { obstacleTargetDist = (TargetCoors - foundCol.point).Magnitude(); Source = foundCol.point; @@ -5238,9 +5240,6 @@ CCam::Process_FollowCar_SA(const CVector& CameraTarget, float TargetOrientation, #endif STARTPATCHES -#ifdef FREE_CAM - Nop(0x468E7B, 0x468E90-0x468E7B); // disable first person -#endif InjectHook(0x456F40, WellBufferMe, PATCH_JUMP); InjectHook(0x458410, &CCam::Init, PATCH_JUMP); InjectHook(0x4582F0, &CCam::GetVectorsReadyForRW, PATCH_JUMP); @@ -5290,6 +5289,4 @@ STARTPATCHES InjectHook(0x456CE0, &FindSplinePathPositionFloat, PATCH_JUMP); InjectHook(0x4569A0, &FindSplinePathPositionVector, PATCH_JUMP); - - InjectHook(0x473250, &CCamera::dtor, PATCH_JUMP); ENDPATCHES diff --git a/src/core/Camera.cpp b/src/core/Camera.cpp index 3f7ed286..e5bc09c8 100644 --- a/src/core/Camera.cpp +++ b/src/core/Camera.cpp @@ -4,40 +4,3097 @@ #include "Draw.h" #include "World.h" #include "Vehicle.h" +#include "Train.h" +#include "Automobile.h" #include "Ped.h" #include "PlayerPed.h" +#include "Wanted.h" #include "Pad.h" +#include "ControllerConfig.h" #include "General.h" #include "ZoneCull.h" #include "SurfaceTable.h" +#include "WaterLevel.h" +#include "World.h" +#include "Garages.h" +#include "Replay.h" +#include "CutsceneMgr.h" +#include "Renderer.h" #include "MBlur.h" +#include "Text.h" +#include "Hud.h" +#include "DMAudio.h" +#include "FileMgr.h" +#include "Frontend.h" +#include "SceneEdit.h" +#include "Pools.h" +#include "Debug.h" #include "Camera.h" +enum +{ + // car + OBBE_WHEEL, + OBBE_1, + OBBE_2, + OBBE_3, + OBBE_1STPERSON, // unused + OBBE_5, + OBBE_ONSTRING, + OBBE_COPCAR, + OBBE_COPCAR_WHEEL, + // ped + OBBE_9, + OBBE_10, + OBBE_11, + OBBE_12, + OBBE_13, + + OBBE_INVALID +}; + +// abbreviate a few things +#define PLAYER (CWorld::Players[CWorld::PlayerInFocus].m_pPed) +// NB: removed explicit TheCamera from all functions + CCamera &TheCamera = *(CCamera*)0x6FACF8; bool &CCamera::m_bUseMouse3rdPerson = *(bool *)0x5F03D8; +bool &bDidWeProcessAnyCinemaCam = *(bool*)0x95CD46; + +#ifdef IMPROVED_CAMERA +#define KEYJUSTDOWN(k) ControlsManager.GetIsKeyboardKeyJustDown((RsKeyCodes)k) +#define KEYDOWN(k) ControlsManager.GetIsKeyboardKeyDown((RsKeyCodes)k) +#define CTRLJUSTDOWN(key) \ + ((KEYDOWN(rsLCTRL) || KEYDOWN(rsRCTRL)) && KEYJUSTDOWN((RsKeyCodes)key) || \ + (KEYJUSTDOWN(rsLCTRL) || KEYJUSTDOWN(rsRCTRL)) && KEYDOWN((RsKeyCodes)key)) +#define CTRLDOWN(key) ((KEYDOWN(rsLCTRL) || KEYDOWN(rsRCTRL)) && KEYDOWN((RsKeyCodes)key)) +#endif + +void +CCamera::Init(void) +{ + memset(this, 0, sizeof(CCamera)); // getting rid of vtable, eh? + + m_pRwCamera = nil; + m_1rstPersonRunCloseToAWall = false; + m_fPositionAlongSpline = 0.0f; + m_bCameraJustRestored = false; + Cams[0].Init(); + Cams[1].Init(); + Cams[2].Init(); + Cams[0].Mode = CCam::MODE_FOLLOWPED; + Cams[1].Mode = CCam::MODE_FOLLOWPED; + unknown = 0; + m_bJustJumpedOutOf1stPersonBecauseOfTarget = 0; + ClearPlayerWeaponMode(); + m_bInATunnelAndABigVehicle = false; + m_iModeObbeCamIsInForCar = OBBE_INVALID; + Cams[0].CamTargetEntity = nil; + Cams[1].CamTargetEntity = nil; + Cams[2].CamTargetEntity = nil; + Cams[0].m_fCamBufferedHeight = 0.0f; + Cams[0].m_fCamBufferedHeightSpeed = 0.0f; + Cams[1].m_fCamBufferedHeight = 0.0f; + Cams[1].m_fCamBufferedHeightSpeed = 0.0f; + Cams[0].m_bCamLookingAtVector = false; + Cams[1].m_bCamLookingAtVector = false; + Cams[2].m_bCamLookingAtVector = false; + Cams[0].m_fPlayerVelocity = 0.0f; + Cams[1].m_fPlayerVelocity = 0.0f; + Cams[2].m_fPlayerVelocity = 0.0f; + m_bHeadBob = false; + m_fFractionInterToStopMovingTarget = 0.25f; + m_fFractionInterToStopCatchUpTarget = 0.75f; + m_fGaitSwayBuffer = 0.85f; + m_bScriptParametersSetForInterPol = false; + m_uiCamShakeStart = 0; + m_fCamShakeForce = 0.0f; + m_iModeObbeCamIsInForCar = OBBE_INVALID; + m_bIgnoreFadingStuffForMusic = false; + m_bWaitForInterpolToFinish = false; + pToGarageWeAreIn = nil; + pToGarageWeAreInForHackAvoidFirstPerson = nil; + m_bPlayerIsInGarage = false; + m_bJustCameOutOfGarage = false; + m_fNearClipScript = DEFAULT_NEAR; + m_bUseNearClipScript = false; + m_vecDoingSpecialInterPolation = false; + m_bAboveGroundTrainNodesLoaded = false; + m_bBelowGroundTrainNodesLoaded = false; + m_WideScreenOn = false; + m_fFOV_Wide_Screen = 0.0f; + m_bRestoreByJumpCut = false; + CarZoomIndicator = 2.0f; + PedZoomIndicator = 2.0f; + CarZoomValueSmooth = 0.0f; + m_fPedZoomValueSmooth = 0.0f; + pTargetEntity = nil; + if(FindPlayerVehicle()) + pTargetEntity = FindPlayerVehicle(); + else + pTargetEntity = CWorld::Players[CWorld::PlayerInFocus].m_pPed; + m_bInitialNodeFound = false; + m_ScreenReductionPercentage = 0.0f; + m_ScreenReductionSpeed = 0.0f; + m_WideScreenOn = false; + m_bWantsToSwitchWidescreenOff = false; + WorldViewerBeingUsed = false; + PlayerExhaustion = 1.0f; + DebugCamMode = CCam::MODE_NONE; + m_PedOrientForBehindOrInFront = 0.0f; + if(!FrontEndMenuManager.m_bStartGameLoading){ + m_bFading = false; + CDraw::FadeValue = 0; + m_fFLOATingFade = 0.0f; + m_bMusicFading = false; + m_fTimeToFadeMusic = 0.0f; + m_fFLOATingFadeMusic = 0.0f; + } + m_bMoveCamToAvoidGeom = false; + if(FrontEndMenuManager.m_bStartGameLoading) + m_bMoveCamToAvoidGeom = true; + m_bStartingSpline = false; + m_iTypeOfSwitch = INTERPOLATION; + m_bUseScriptZoomValuePed = false; + m_bUseScriptZoomValueCar = false; + m_fPedZoomValueScript = 0.0f; + m_fCarZoomValueScript = 0.0f; + m_bUseSpecialFovTrain = false; + m_fFovForTrain = 70.0f; // or DefaultFOV from Cam.cpp + m_iModeToGoTo = CCam::MODE_FOLLOWPED; + m_bJust_Switched = false; + m_bUseTransitionBeta = false; + m_matrix.SetScale(1.0f); + m_bTargetJustBeenOnTrain = false; + m_bInitialNoNodeStaticsSet = false; + m_uiLongestTimeInMill = 5000; + m_uiTimeLastChange = 0; + m_uiTimeWeEnteredIdle = 0; + m_bIdleOn = false; + LODDistMultiplier = 1.0f; + m_bCamDirectlyBehind = false; + m_bCamDirectlyInFront = false; + m_motionBlur = 0; + m_bGarageFixedCamPositionSet = false; + SetMotionBlur(255, 255, 255, 0, 0); + m_bCullZoneChecksOn = false; + m_bFailedCullZoneTestPreviously = false; + m_iCheckCullZoneThisNumFrames = 6; + m_iZoneCullFrameNumWereAt = 0; + m_CameraAverageSpeed = 0.0f; + m_CameraSpeedSoFar = 0.0f; + m_PreviousCameraPosition = CVector(0.0f, 0.0f, 0.0f); + m_iWorkOutSpeedThisNumFrames = 4; + m_iNumFramesSoFar = 0; + m_bJustInitalised = true; + m_uiTransitionState = 0; + m_uiTimeTransitionStart = 0; + m_bLookingAtPlayer = true; + m_fMouseAccelHorzntl = 0.0025f; + m_fMouseAccelVertical = 0.003f; + m_f3rdPersonCHairMultX = 0.53f; + m_f3rdPersonCHairMultY = 0.4f; +} + +void +CCamera::Process(void) +{ + // static bool InterpolatorNotInitialised = true; // unused + static float PlayerMinDist = 1.6f; // not on PS2 + static bool WasPreviouslyInterSyhonFollowPed = false; // only written + float FOV = 0.0f; + float oldBeta, newBeta; + float deltaBeta = 0.0f; + bool lookLRBVehicle = false; + CVector CamFront, CamUp, CamSource, Target; + + m_bJust_Switched = false; + m_RealPreviousCameraPosition = GetPosition(); + + // Update target entity + if(m_bLookingAtPlayer || m_bTargetJustBeenOnTrain || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) + UpdateTargetEntity(); + if(pTargetEntity == nil) + pTargetEntity = FindPlayerPed(); + if(Cams[ActiveCam].CamTargetEntity == nil) + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + if(Cams[(ActiveCam+1)%2].CamTargetEntity == nil) + Cams[(ActiveCam+1)%2].CamTargetEntity = pTargetEntity; + + CamControl(); + if(m_bFading) + ProcessFade(); + if(m_bMusicFading) + ProcessMusicFade(); + if(m_WideScreenOn) + ProcessWideScreenOn(); + +#ifdef IMPROVED_CAMERA + if(CPad::GetPad(1)->GetCircleJustDown() || CTRLJUSTDOWN('B')){ +#else + if(CPad::GetPad(1)->GetCircleJustDown()){ +#endif + WorldViewerBeingUsed = !WorldViewerBeingUsed; + if(WorldViewerBeingUsed) + InitialiseCameraForDebugMode(); + else + CPad::m_bMapPadOneToPadTwo = false; + } + + RwCameraSetNearClipPlane(Scene.camera, DEFAULT_NEAR); + + if(Cams[ActiveCam].Front.x == 0.0f && Cams[ActiveCam].Front.y == 0.0f) + oldBeta = 0.0f; + else + oldBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + + Cams[ActiveCam].Process(); + Cams[ActiveCam].ProcessSpecialHeightRoutines(); + + if(Cams[ActiveCam].Front.x == 0.0f && Cams[ActiveCam].Front.y == 0.0f) + newBeta = 0.0f; + else + newBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + + + // Stop transition when it's done + if(m_uiTransitionState != 0){ +/* + // PS2: + if(!m_bWaitForInterpolToFinish){ + Cams[(ActiveCam+1)%2].Process(); + Cams[(ActiveCam+1)%2].ProcessSpecialHeightRoutines(); + } +*/ + // not PS2 (done in CamControl there it seems) + if(CTimer::GetTimeInMilliseconds() > m_uiTransitionDuration+m_uiTimeTransitionStart){ + m_uiTransitionState = 0; + m_vecDoingSpecialInterPolation = false; + m_bWaitForInterpolToFinish = false; + } + } + + if(m_bUseNearClipScript) + RwCameraSetNearClipPlane(Scene.camera, m_fNearClipScript); + + deltaBeta = newBeta - oldBeta; + while(deltaBeta >= PI) deltaBeta -= 2*PI; + while(deltaBeta < -PI) deltaBeta += 2*PI; + if(Abs(deltaBeta) > 0.3f) + m_bJust_Switched = true; + + // Debug stuff + if(!gbModelViewer) + Cams[ActiveCam].PrintMode(); + if(WorldViewerBeingUsed) + Cams[2].Process(); + + if(Cams[ActiveCam].DirectionWasLooking != LOOKING_FORWARD && pTargetEntity->IsVehicle()) + lookLRBVehicle = true; + + if(m_uiTransitionState != 0 && !lookLRBVehicle){ + // Process transition + // different on PS2 + + uint32 currentTime = CTimer::GetTimeInMilliseconds() - m_uiTimeTransitionStart; + if(currentTime >= m_uiTransitionDuration) + currentTime = m_uiTransitionDuration; + float fractionInter = (float) currentTime / m_uiTransitionDuration; + + if(fractionInter <= m_fFractionInterToStopMovingTarget){ + float inter; + if(m_fFractionInterToStopMovingTarget == 0.0f) + inter = 0.0f; + else + inter = (m_fFractionInterToStopMovingTarget - fractionInter)/m_fFractionInterToStopMovingTarget; + inter = 0.5f - 0.5*Cos(inter*PI); // smooth it + + m_vecSourceWhenInterPol = m_cvecStartingSourceForInterPol + inter*m_cvecSourceSpeedAtStartInter; + m_vecTargetWhenInterPol = m_cvecStartingTargetForInterPol + inter*m_cvecTargetSpeedAtStartInter; + m_vecUpWhenInterPol = m_cvecStartingUpForInterPol + inter*m_cvecUpSpeedAtStartInter; + m_fFOVWhenInterPol = m_fStartingFOVForInterPol + inter*m_fFOVSpeedAtStartInter; + + CamSource = m_vecSourceWhenInterPol; + + if(m_bItsOkToLookJustAtThePlayer){ + m_vecTargetWhenInterPol.x = FindPlayerPed()->GetPosition().x; + m_vecTargetWhenInterPol.y = FindPlayerPed()->GetPosition().y; + m_fBetaWhenInterPol = m_fStartingBetaForInterPol + inter*m_fBetaSpeedAtStartInter; + + float dist = (CamSource - m_vecTargetWhenInterPol).Magnitude2D(); + if(dist < PlayerMinDist){ + if(dist > 0.0f){ + CamSource.x = m_vecTargetWhenInterPol.x + PlayerMinDist*Cos(m_fBetaWhenInterPol); + CamSource.y = m_vecTargetWhenInterPol.y + PlayerMinDist*Sin(m_fBetaWhenInterPol); + }else{ + // can only be 0.0 now... + float beta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + CamSource.x = m_vecTargetWhenInterPol.x + PlayerMinDist*Cos(beta); + CamSource.y = m_vecTargetWhenInterPol.y + PlayerMinDist*Sin(beta); + } + }else{ + CamSource.x = m_vecTargetWhenInterPol.x + dist*Cos(m_fBetaWhenInterPol); + CamSource.y = m_vecTargetWhenInterPol.y + dist*Sin(m_fBetaWhenInterPol); + } + } + + CamFront = m_vecTargetWhenInterPol - CamSource; + StoreValuesDuringInterPol(CamSource, m_vecTargetWhenInterPol, m_vecUpWhenInterPol, m_fFOVWhenInterPol); + Target = m_vecTargetWhenInterPol; + CamFront.Normalise(); + if(m_bLookingAtPlayer) + CamUp = CVector(0.0f, 0.0f, 1.0f); + else + CamUp = m_vecUpWhenInterPol; + CamUp.Normalise(); + + if(Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN || Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ + CamFront.Normalise(); + CamUp = CrossProduct(CamFront, CVector(-1.0f, 0.0f, 0.0f)); + }else{ + CamFront.Normalise(); + CamUp.Normalise(); + CVector right = CrossProduct(CamFront, CamUp); + right.Normalise(); + CamUp = CrossProduct(right, CamFront); + } + CamUp.Normalise(); + FOV = m_fFOVWhenInterPol; + }else if(fractionInter > m_fFractionInterToStopMovingTarget && fractionInter <= 1.0f){ + float inter; + if(m_fFractionInterToStopCatchUpTarget == 0.0f) + inter = 0.0f; + else + inter = (fractionInter - m_fFractionInterToStopMovingTarget)/m_fFractionInterToStopCatchUpTarget; + inter = 0.5f - 0.5*Cos(inter*PI); // smooth it + + CamSource = m_vecSourceWhenInterPol + inter*(Cams[ActiveCam].Source - m_vecSourceWhenInterPol); + FOV = m_fFOVWhenInterPol + inter*(Cams[ActiveCam].FOV - m_fFOVWhenInterPol); + Target = m_vecTargetWhenInterPol + inter*(Cams[ActiveCam].m_cvecTargetCoorsForFudgeInter - m_vecTargetWhenInterPol); + CamUp = m_vecUpWhenInterPol + inter*(Cams[ActiveCam].Up - m_vecUpWhenInterPol); + deltaBeta = Cams[ActiveCam].m_fTrueBeta - m_fBetaWhenInterPol; + MakeAngleLessThan180(deltaBeta); + float interpBeta = m_fBetaWhenInterPol + inter*deltaBeta; + + if(m_bItsOkToLookJustAtThePlayer){ + Target.x = FindPlayerPed()->GetPosition().x; + Target.y = FindPlayerPed()->GetPosition().y; + + float dist = (CamSource - Target).Magnitude2D(); + if(dist < PlayerMinDist){ + if(dist > 0.0f){ + CamSource.x = Target.x + PlayerMinDist*Cos(interpBeta); + CamSource.y = Target.y + PlayerMinDist*Sin(interpBeta); + }else{ + // can only be 0.0 now... + float beta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + CamSource.x = Target.x + PlayerMinDist*Cos(beta); + CamSource.y = Target.y + PlayerMinDist*Sin(beta); + } + }else{ + CamSource.x = Target.x + dist*Cos(interpBeta); + CamSource.y = Target.y + dist*Sin(interpBeta); + } + } + + CamFront = Target - CamSource; + StoreValuesDuringInterPol(CamSource, Target, CamUp, FOV); + CamFront.Normalise(); + if(m_bLookingAtPlayer) + CamUp = CVector(0.0f, 0.0f, 1.0f); + + if(Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN || Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ + CamFront.Normalise(); + CamUp = CrossProduct(CamFront, CVector(-1.0f, 0.0f, 0.0f)); + }else{ + CamFront.Normalise(); + CamUp.Normalise(); + CVector right = CrossProduct(CamFront, CamUp); + right.Normalise(); + CamUp = CrossProduct(right, CamFront); + } + CamUp.Normalise(); +#ifndef FIX_BUGS + // BUG: FOV was already interpolated but m_fFOVWhenInterPol was not + FOV = m_fFOVWhenInterPol; +#endif + } + + CVector Dist = CamSource - Target; + float DistOnGround = Dist.Magnitude2D(); + float Alpha = CGeneral::GetATanOfXY(DistOnGround, Dist.z); + float Beta = CGeneral::GetATanOfXY(Dist.x, Dist.y); + Cams[ActiveCam].KeepTrackOfTheSpeed(CamSource, Target, CamUp, Alpha, Beta, FOV); + }else{ + // No transition, take Cam values directly + if(WorldViewerBeingUsed){ + CamSource = Cams[2].Source; + CamFront = Cams[2].Front; + CamUp = Cams[2].Up; + FOV = Cams[2].FOV; + }else{ + CamSource = Cams[ActiveCam].Source; + CamFront = Cams[ActiveCam].Front; + CamUp = Cams[ActiveCam].Up; + FOV = Cams[ActiveCam].FOV; + } + WasPreviouslyInterSyhonFollowPed = false; // unused + } + + if(m_uiTransitionState != 0) + if(!m_bLookingAtVector && m_bLookingAtPlayer && !CCullZones::CamStairsForPlayer() && !m_bPlayerIsInGarage){ + CEntity *entity = nil; + CColPoint colPoint; + if(CWorld::ProcessLineOfSight(pTargetEntity->GetPosition(), CamSource, colPoint, entity, true, false, false, true, false, true, true)){ + CamSource = colPoint.point; + RwCameraSetNearClipPlane(Scene.camera, 0.05f); + } + } + + GetRight() = CrossProduct(CamUp, CamFront); // actually Left + GetForward() = CamFront; + GetUp() = CamUp; + GetPosition() = CamSource; + + // Process Shake + float shakeStrength = m_fCamShakeForce - 0.28f*(CTimer::GetTimeInMilliseconds()-m_uiCamShakeStart)/1000.0f; + shakeStrength = clamp(shakeStrength, 0.0f, 2.0f); + int shakeRand = CGeneral::GetRandomNumber(); + float shakeOffset = shakeStrength*0.1f; + GetPosition().x += shakeOffset*((shakeRand&0xF)-7); + GetPosition().y += shakeOffset*(((shakeRand&0xF0)>>4)-7); + GetPosition().z += shakeOffset*(((shakeRand&0xF00)>>8)-7); + + if(shakeOffset > 0.0f && m_BlurType != MBLUR_SNIPER) + SetMotionBlurAlpha(min((int)(shakeStrength*255.0f) + 25, 150)); + if(Cams[ActiveCam].Mode == CCam::MODE_1STPERSON && FindPlayerVehicle() && FindPlayerVehicle()->GetUp().z < 0.2f) + SetMotionBlur(230, 230, 230, 215, MBLUR_NORMAL); + + CalculateDerivedValues(); + CDraw::SetFOV(FOV); + + // Set RW camera + if(WorldViewerBeingUsed){ + RwFrame *frame = RwCameraGetFrame(m_pRwCamera); + CVector Source = Cams[2].Source; + CVector Front = Cams[2].Front; + CVector Up = Cams[2].Up; + + GetRight() = CrossProduct(Up, Front); + GetForward() = Front; + GetUp() = Up; + GetPosition() = Source; + + CDraw::SetFOV(Cams[2].FOV); + m_vecGameCamPos = Cams[ActiveCam].Source; + + *RwMatrixGetPos(RwFrameGetMatrix(frame)) = (RwV3d)GetPosition(); + *RwMatrixGetAt(RwFrameGetMatrix(frame)) = (RwV3d)GetForward(); + *RwMatrixGetUp(RwFrameGetMatrix(frame)) = (RwV3d)GetUp(); + *RwMatrixGetRight(RwFrameGetMatrix(frame)) = (RwV3d)GetRight(); + RwMatrixUpdate(RwFrameGetMatrix(frame)); + RwFrameUpdateObjects(frame); + }else{ + RwFrame *frame = RwCameraGetFrame(m_pRwCamera); + m_vecGameCamPos = GetPosition(); + *RwMatrixGetPos(RwFrameGetMatrix(frame)) = (RwV3d)GetPosition(); + *RwMatrixGetAt(RwFrameGetMatrix(frame)) = (RwV3d)GetForward(); + *RwMatrixGetUp(RwFrameGetMatrix(frame)) = (RwV3d)GetUp(); + *RwMatrixGetRight(RwFrameGetMatrix(frame)) = (RwV3d)GetRight(); + RwMatrixUpdate(RwFrameGetMatrix(frame)); + RwFrameUpdateObjects(frame); + } + + CDraw::SetNearClipZ(RwCameraGetNearClipPlane(m_pRwCamera)); + CDraw::SetFarClipZ(RwCameraGetFarClipPlane(m_pRwCamera)); + + UpdateSoundDistances(); + + if((CTimer::GetFrameCounter()&0xF) == 3) + DistanceToWater = CWaterLevel::CalcDistanceToWater(GetPosition().x, GetPosition().y); + + // LOD dist + if(!CCutsceneMgr::IsRunning() || CCutsceneMgr::UseLodMultiplier()) + LODDistMultiplier = 70.0f/CDraw::GetFOV() * CDraw::GetAspectRatio()/(4.0f/3.0f); + else + LODDistMultiplier = 1.0f; + GenerationDistMultiplier = LODDistMultiplier; + LODDistMultiplier *= CRenderer::ms_lodDistScale; + + // Keep track of speed + if(m_bJustInitalised || m_bJust_Switched){ + m_PreviousCameraPosition = GetPosition(); + m_bJustInitalised = false; + } + m_CameraSpeedSoFar += (GetPosition() - m_PreviousCameraPosition).Magnitude(); + m_iNumFramesSoFar++; + if(m_iNumFramesSoFar == m_iWorkOutSpeedThisNumFrames){ + m_CameraAverageSpeed = m_CameraSpeedSoFar / m_iWorkOutSpeedThisNumFrames; + m_CameraSpeedSoFar = 0.0f; + m_iNumFramesSoFar = 0; + } + m_PreviousCameraPosition = GetPosition(); + + // PS2: something doing on with forward vector here + + if(Cams[ActiveCam].DirectionWasLooking != LOOKING_FORWARD && Cams[ActiveCam].Mode != CCam::MODE_TOP_DOWN_PED){ + Cams[ActiveCam].Source = Cams[ActiveCam].SourceBeforeLookBehind; + Orientation += PI; + } + + if(m_uiTransitionState != 0){ + int OtherCam = (ActiveCam+1)%2; + if(Cams[OtherCam].CamTargetEntity && + pTargetEntity && pTargetEntity->IsPed() && + !Cams[OtherCam].CamTargetEntity->IsVehicle() && + Cams[ActiveCam].Mode != CCam::MODE_TOP_DOWN_PED && Cams[ActiveCam].DirectionWasLooking != LOOKING_FORWARD){ + Cams[OtherCam].Source = Cams[ActiveCam%2].SourceBeforeLookBehind; + Orientation += PI; + } + } + + m_bCameraJustRestored = false; +} + +void +CCamera::CamControl(void) +{ + static bool PlaceForFixedWhenSniperFound = false; + static int16 ReqMode; + bool disableGarageCam = false; + bool switchByJumpCut = false; + bool stairs = false; + bool boatTarget = false; + CVector targetPos; + CVector garageCenter, garageDoorPos1, garageDoorPos2; + CVector garageCenterToDoor, garageCamPos; + int whichDoor; + + m_bObbeCinematicPedCamOn = false; + m_bObbeCinematicCarCamOn = false; + m_bUseTransitionBeta = false; + m_bUseSpecialFovTrain = false; + m_bJustCameOutOfGarage = false; + m_bTargetJustCameOffTrain = false; + m_bInATunnelAndABigVehicle = false; + + if(Cams[ActiveCam].CamTargetEntity == nil && pTargetEntity == nil) + pTargetEntity = PLAYER; + + m_iZoneCullFrameNumWereAt++; + if(m_iZoneCullFrameNumWereAt > m_iCheckCullZoneThisNumFrames) + m_iZoneCullFrameNumWereAt = 1; + m_bCullZoneChecksOn = m_iZoneCullFrameNumWereAt == m_iCheckCullZoneThisNumFrames; + if(m_bCullZoneChecksOn) + m_bFailedCullZoneTestPreviously = CCullZones::CamCloseInForPlayer(); + + if(m_bLookingAtPlayer){ + CPad::GetPad(0)->DisablePlayerControls &= ~PLAYERCONTROL_DISABLED_1; + FindPlayerPed()->bIsVisible = true; + } + + if(!CTimer::GetIsPaused()){ + float CloseInCarHeightTarget = 0.0f; + float CloseInPedHeightTarget = 0.0f; + + if(m_bTargetJustBeenOnTrain){ + // Getting off train + if(!pTargetEntity->IsVehicle() || !((CVehicle*)pTargetEntity)->IsTrain()){ + Restore(); + m_bTargetJustCameOffTrain = true; + m_bTargetJustBeenOnTrain = false; + SetWideScreenOff(); + } + } + + // Vehicle target + if(pTargetEntity->IsVehicle()){ + if(((CVehicle*)pTargetEntity)->IsTrain()){ + if(!m_bTargetJustBeenOnTrain){ + m_bInitialNodeFound = false; + m_bInitialNoNodeStaticsSet = false; + } + Process_Train_Camera_Control(); + }else{ + if(((CVehicle*)pTargetEntity)->IsBoat()) + boatTarget = true; + + // Change user selected mode + if(CPad::GetPad(0)->CycleCameraModeUpJustDown() && !CReplay::IsPlayingBack() && + (m_bLookingAtPlayer || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) && + !m_WideScreenOn) + CarZoomIndicator -= 1.0f; + if(CPad::GetPad(0)->CycleCameraModeDownJustDown() && !CReplay::IsPlayingBack() && + (m_bLookingAtPlayer || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) && + !m_WideScreenOn) + CarZoomIndicator += 1.0f; + if(!m_bFailedCullZoneTestPreviously){ + if(CarZoomIndicator < CAM_ZOOM_1STPRS) CarZoomIndicator = CAM_ZOOM_CINEMATIC; + else if(CarZoomIndicator > CAM_ZOOM_CINEMATIC) CarZoomIndicator = CAM_ZOOM_1STPRS; + } + + if(m_bFailedCullZoneTestPreviously) + if(CarZoomIndicator != CAM_ZOOM_1STPRS && CarZoomIndicator != CAM_ZOOM_TOPDOWN) + ReqMode = CCam::MODE_CAM_ON_A_STRING; + + switch(((CVehicle*)pTargetEntity)->m_vehType){ + case VEHICLE_TYPE_CAR: + case VEHICLE_TYPE_BIKE: + if(CGarages::IsPointInAGarageCameraZone(pTargetEntity->GetPosition())){ + if(!m_bGarageFixedCamPositionSet && m_bLookingAtPlayer || + WhoIsInControlOfTheCamera == CAMCONTROL_OBBE){ + if(pToGarageWeAreIn){ + float ground; + bool foundGround; + + // This is all very strange.... + // targetPos = pTargetEntity->GetPosition(); // unused + if(pToGarageWeAreIn->m_pDoor1){ + whichDoor = 1; + garageDoorPos1.x = pToGarageWeAreIn->m_fDoor1X; + garageDoorPos1.y = pToGarageWeAreIn->m_fDoor1Y; + garageDoorPos1.z = 0.0f; + // targetPos.z = 0.0f; // unused + // (targetPos - doorPos1).Magnitude(); // unused + }else if(pToGarageWeAreIn->m_pDoor2){ + whichDoor = 2; +#ifdef FIX_BUGS + garageDoorPos2.x = pToGarageWeAreIn->m_fDoor2X; + garageDoorPos2.y = pToGarageWeAreIn->m_fDoor2Y; + garageDoorPos2.z = 0.0f; +#endif + }else{ + whichDoor = 1; + garageDoorPos1.x = pTargetEntity->GetPosition().x; + garageDoorPos1.y = pTargetEntity->GetPosition().y; +#ifdef FIX_BUGS + garageDoorPos1.z = 0.0f; +#else + garageDoorPos2.z = 0.0f; +#endif + } + garageCenter.x = (pToGarageWeAreIn->m_fX1 + pToGarageWeAreIn->m_fX2)/2.0f; + garageCenter.y = (pToGarageWeAreIn->m_fY1 + pToGarageWeAreIn->m_fY2)/2.0f; + garageCenter.z = 0.0f; + if(whichDoor == 1) + garageCenterToDoor = garageDoorPos1 - garageCenter; + else + garageCenterToDoor = garageDoorPos2 - garageCenter; + targetPos = pTargetEntity->GetPosition(); + ground = CWorld::FindGroundZFor3DCoord(targetPos.x, targetPos.y, targetPos.z, &foundGround); + if(!foundGround) + ground = targetPos.z - 0.2f; + garageCenterToDoor.z = 0.0f; + garageCenterToDoor.Normalise(); + if(whichDoor == 1) + garageCamPos = garageDoorPos1 + 13.0f*garageCenterToDoor; + else + garageCamPos = garageDoorPos2 + 13.0f*garageCenterToDoor; + garageCamPos.z = ground + 3.1f; + SetCamPositionForFixedMode(garageCamPos, CVector(0.0f, 0.0f, 0.0f)); + m_bGarageFixedCamPositionSet = true; + } + } + + if(CGarages::CameraShouldBeOutside() && m_bGarageFixedCamPositionSet && + (m_bLookingAtPlayer || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE)){ + if(pToGarageWeAreIn){ + ReqMode = CCam::MODE_FIXED; + m_bPlayerIsInGarage = true; + } + }else{ + if(m_bPlayerIsInGarage){ + m_bJustCameOutOfGarage = true; + m_bPlayerIsInGarage = false; + } + ReqMode = CCam::MODE_CAM_ON_A_STRING; + } + }else{ + if(m_bPlayerIsInGarage){ + m_bJustCameOutOfGarage = true; + m_bPlayerIsInGarage = false; + } + m_bGarageFixedCamPositionSet = false; + ReqMode = CCam::MODE_CAM_ON_A_STRING; + } + break; + case VEHICLE_TYPE_BOAT: + ReqMode = CCam::MODE_BEHINDBOAT; + break; + } + + // Car zoom value + if(CarZoomIndicator == CAM_ZOOM_1STPRS && !m_bPlayerIsInGarage){ + CarZoomValue = 0.0f; + ReqMode = CCam::MODE_1STPERSON; + }else if(CarZoomIndicator == CAM_ZOOM_1) + CarZoomValue = 0.05f; + else if(CarZoomIndicator == CAM_ZOOM_2) + CarZoomValue = 1.9f; + else if(CarZoomIndicator == CAM_ZOOM_3) + CarZoomValue = 3.9f; + if(CarZoomIndicator == CAM_ZOOM_TOPDOWN && !m_bPlayerIsInGarage){ + CarZoomValue = 1.0f; + ReqMode = CCam::MODE_TOPDOWN; + } + + // Check if we have to go into first person + if(((CVehicle*)pTargetEntity)->IsCar() && !m_bPlayerIsInGarage){ + if(CCullZones::Cam1stPersonForPlayer() && + pTargetEntity->GetColModel()->boundingBox.GetSize().z >= 3.026f && + pToGarageWeAreInForHackAvoidFirstPerson == nil){ + ReqMode = CCam::MODE_1STPERSON; + m_bInATunnelAndABigVehicle = true; + } + } + if(ReqMode == CCam::MODE_TOPDOWN && + (CCullZones::Cam1stPersonForPlayer() || CCullZones::CamNoRain() || CCullZones::PlayerNoRain())) + ReqMode = CCam::MODE_1STPERSON; + + // Smooth zoom value - ugly code + if(m_bUseScriptZoomValueCar){ + if(CarZoomValueSmooth < m_fCarZoomValueScript){ + CarZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = min(CarZoomValueSmooth, m_fCarZoomValueScript); + }else{ + CarZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = max(CarZoomValueSmooth, m_fCarZoomValueScript); + } + }else if(m_bFailedCullZoneTestPreviously){ + CloseInCarHeightTarget = 0.65f; + if(CarZoomValueSmooth < -0.65f){ + CarZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = min(CarZoomValueSmooth, -0.65f); + }else{ + CarZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = max(CarZoomValueSmooth, -0.65f); + } + }else{ + if(CarZoomValueSmooth < CarZoomValue){ + CarZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = min(CarZoomValueSmooth, CarZoomValue); + }else{ + CarZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + CarZoomValueSmooth = max(CarZoomValueSmooth, CarZoomValue); + } + } + + WellBufferMe(CloseInCarHeightTarget, &Cams[ActiveCam].m_fCloseInCarHeightOffset, &Cams[ActiveCam].m_fCloseInCarHeightOffsetSpeed, 0.1f, 0.25f, false); + + // Fallen into water + if(Cams[ActiveCam].IsTargetInWater(Cams[ActiveCam].Source) && !boatTarget && + !Cams[ActiveCam].CamTargetEntity->IsPed()) + ReqMode = CCam::MODE_PLAYER_FALLEN_WATER; + } + } + + // Ped target + else if(pTargetEntity->IsPed()){ + // Change user selected mode + if(CPad::GetPad(0)->CycleCameraModeUpJustDown() && !CReplay::IsPlayingBack() && + (m_bLookingAtPlayer || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) && + !m_WideScreenOn && !m_bFailedCullZoneTestPreviously){ + if(FrontEndMenuManager.m_ControlMethod == CONTROL_STANDARD){ + if(PedZoomIndicator == CAM_ZOOM_TOPDOWN) + PedZoomIndicator = CAM_ZOOM_1; + else + PedZoomIndicator = CAM_ZOOM_TOPDOWN; + }else + PedZoomIndicator -= 1.0f; + } + if(CPad::GetPad(0)->CycleCameraModeDownJustDown() && !CReplay::IsPlayingBack() && + (m_bLookingAtPlayer || WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) && + !m_WideScreenOn && !m_bFailedCullZoneTestPreviously){ + if(FrontEndMenuManager.m_ControlMethod == CONTROL_STANDARD){ + if(PedZoomIndicator == CAM_ZOOM_TOPDOWN) + PedZoomIndicator = CAM_ZOOM_1; + else + PedZoomIndicator = CAM_ZOOM_TOPDOWN; + }else + PedZoomIndicator += 1.0f; + } + // disabled obbe's cam here + if(PedZoomIndicator < CAM_ZOOM_1) PedZoomIndicator = CAM_ZOOM_TOPDOWN; + else if(PedZoomIndicator > CAM_ZOOM_TOPDOWN) PedZoomIndicator = CAM_ZOOM_1; + + ReqMode = CCam::MODE_FOLLOWPED; + + // Check 1st person mode + if(m_bLookingAtPlayer && pTargetEntity->IsPed() && !m_WideScreenOn && !Cams[0].Using3rdPersonMouseCam() +#ifdef FREE_CAM + && !CCamera::bFreeCam +#endif + ){ + // See if we want to enter first person mode + if(CPad::GetPad(0)->LookAroundLeftRight() || CPad::GetPad(0)->LookAroundUpDown()){ + m_uiFirstPersonCamLastInputTime = CTimer::GetTimeInMilliseconds(); + m_bFirstPersonBeingUsed = true; + }else if(m_bFirstPersonBeingUsed){ + // Or if we want to go back to 3rd person + if(CPad::GetPad(0)->GetPedWalkLeftRight() || CPad::GetPad(0)->GetPedWalkUpDown() || + CPad::GetPad(0)->GetSquare() || CPad::GetPad(0)->GetTriangle() || + CPad::GetPad(0)->GetCross() || CPad::GetPad(0)->GetCircle() || + CTimer::GetTimeInMilliseconds() - m_uiFirstPersonCamLastInputTime > 2850.0f) + m_bFirstPersonBeingUsed = false; + } + }else + m_bFirstPersonBeingUsed = false; + + if(!FindPlayerPed()->IsPedInControl() || FindPlayerPed()->m_fMoveSpeed > 0.0f) + m_bFirstPersonBeingUsed = false; + if(m_bFirstPersonBeingUsed){ + ReqMode = CCam::MODE_1STPERSON; + CPad::GetPad(0)->DisablePlayerControls |= PLAYERCONTROL_DISABLED_1; + } + + // Zoom value + if(PedZoomIndicator == CAM_ZOOM_1) + m_fPedZoomValue = 0.25f; + else if(PedZoomIndicator == CAM_ZOOM_2) + m_fPedZoomValue = 1.5f; + else if(PedZoomIndicator == CAM_ZOOM_3) + m_fPedZoomValue = 2.9f; + + // Smooth zoom value - ugly code + if(m_bUseScriptZoomValuePed){ + if(m_fPedZoomValueSmooth < m_fPedZoomValueScript){ + m_fPedZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = min(m_fPedZoomValueSmooth, m_fPedZoomValueScript); + }else{ + m_fPedZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = max(m_fPedZoomValueSmooth, m_fPedZoomValueScript); + } + }else if(m_bFailedCullZoneTestPreviously){ + static float PedZoomedInVal = 0.5f; + CloseInPedHeightTarget = 0.7f; + if(m_fPedZoomValueSmooth < PedZoomedInVal){ + m_fPedZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = min(m_fPedZoomValueSmooth, PedZoomedInVal); + }else{ + m_fPedZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = max(m_fPedZoomValueSmooth, PedZoomedInVal); + } + }else{ + if(m_fPedZoomValueSmooth < m_fPedZoomValue){ + m_fPedZoomValueSmooth += 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = min(m_fPedZoomValueSmooth, m_fPedZoomValue); + }else{ + m_fPedZoomValueSmooth -= 0.12f * CTimer::GetTimeStep(); + m_fPedZoomValueSmooth = max(m_fPedZoomValueSmooth, m_fPedZoomValue); + } + } + + WellBufferMe(CloseInPedHeightTarget, &Cams[ActiveCam].m_fCloseInPedHeightOffset, &Cams[ActiveCam].m_fCloseInPedHeightOffsetSpeed, 0.1f, 0.025f, false); + + // Check if entering fight cam + if(!m_bFirstPersonBeingUsed){ + if(FindPlayerPed()->GetPedState() == PED_FIGHT && !m_bUseMouse3rdPerson) + ReqMode = CCam::MODE_FIGHT_CAM; + if(((CPed*)pTargetEntity)->GetWeapon()->m_eWeaponType == WEAPONTYPE_BASEBALLBAT && + FindPlayerPed()->GetPedState() == PED_ATTACK && !m_bUseMouse3rdPerson) + ReqMode = CCam::MODE_FIGHT_CAM; + } + + // Garage cam + if(CCullZones::CamStairsForPlayer() && CCullZones::FindZoneWithStairsAttributeForPlayer()) + stairs = true; + // Some hack for Mr Whoopee in a bomb shop + if(Cams[ActiveCam].Using3rdPersonMouseCam() && CCollision::ms_collisionInMemory == LEVEL_COMMERCIAL){ + if(pTargetEntity->GetPosition().x < 83.0f && pTargetEntity->GetPosition().x > 18.0f && + pTargetEntity->GetPosition().y < -305.0f && pTargetEntity->GetPosition().y > -390.0f) + disableGarageCam = true; + } + if(!disableGarageCam && (CGarages::IsPointInAGarageCameraZone(pTargetEntity->GetPosition()) || stairs)){ + if(!m_bGarageFixedCamPositionSet && m_bLookingAtPlayer){ + if(pToGarageWeAreIn || stairs){ + float ground; + bool foundGround; + + if(pToGarageWeAreIn){ + // targetPos = pTargetEntity->GetPosition(); // unused + if(pToGarageWeAreIn->m_pDoor1){ + whichDoor = 1; + garageDoorPos1.x = pToGarageWeAreIn->m_fDoor1X; + garageDoorPos1.y = pToGarageWeAreIn->m_fDoor1Y; + garageDoorPos1.z = 0.0f; + // targetPos.z = 0.0f; // unused + // (targetPos - doorPos1).Magnitude(); // unused + }else if(pToGarageWeAreIn->m_pDoor2){ + whichDoor = 2; +#ifdef FIX_BUGS + garageDoorPos2.x = pToGarageWeAreIn->m_fDoor2X; + garageDoorPos2.y = pToGarageWeAreIn->m_fDoor2Y; + garageDoorPos2.z = 0.0f; +#endif + }else{ + whichDoor = 1; + garageDoorPos1.x = pTargetEntity->GetPosition().x; + garageDoorPos1.y = pTargetEntity->GetPosition().y; +#ifdef FIX_BUGS + garageDoorPos1.z = 0.0f; +#else + garageDoorPos2.z = 0.0f; +#endif + } + }else{ + whichDoor = 1; + garageDoorPos1 = Cams[ActiveCam].Source; + } + + if(pToGarageWeAreIn){ + garageCenter.x = (pToGarageWeAreIn->m_fX1 + pToGarageWeAreIn->m_fX2)/2.0f; + garageCenter.y = (pToGarageWeAreIn->m_fY1 + pToGarageWeAreIn->m_fY2)/2.0f; + garageCenter.z = 0.0f; + }else{ + garageDoorPos1.z = 0.0f; + if(stairs){ + CAttributeZone *az = CCullZones::FindZoneWithStairsAttributeForPlayer(); + garageCenter.x = (az->minx + az->maxx)/2.0f; + garageCenter.y = (az->miny + az->maxy)/2.0f; + garageCenter.z = 0.0f; + }else + garageCenter = pTargetEntity->GetPosition(); + } + if(whichDoor == 1) + garageCenterToDoor = garageDoorPos1 - garageCenter; + else + garageCenterToDoor = garageDoorPos2 - garageCenter; + targetPos = pTargetEntity->GetPosition(); + ground = CWorld::FindGroundZFor3DCoord(targetPos.x, targetPos.y, targetPos.z, &foundGround); + if(!foundGround) + ground = targetPos.z - 0.2f; + garageCenterToDoor.z = 0.0f; + garageCenterToDoor.Normalise(); + if(whichDoor == 1){ + if(pToGarageWeAreIn == nil && stairs) + garageCamPos = garageDoorPos1 + 3.75f*garageCenterToDoor; + else + garageCamPos = garageDoorPos1 + 13.0f*garageCenterToDoor; + }else{ + garageCamPos = garageDoorPos2 + 13.0f*garageCenterToDoor; + } + if(PedZoomIndicator == CAM_ZOOM_TOPDOWN && !stairs){ + garageCamPos = garageCenter; + garageCamPos.z += FindPlayerPed()->GetPosition().z + 2.1f; + if(pToGarageWeAreIn && garageCamPos.z > pToGarageWeAreIn->m_fX2) // What? + garageCamPos.z = pToGarageWeAreIn->m_fX2; + }else + garageCamPos.z = ground + 3.1f; + SetCamPositionForFixedMode(garageCamPos, CVector(0.0f, 0.0f, 0.0f)); + m_bGarageFixedCamPositionSet = true; + } + } + + if((CGarages::CameraShouldBeOutside() || stairs) && m_bLookingAtPlayer && m_bGarageFixedCamPositionSet){ + if(pToGarageWeAreIn || stairs){ + ReqMode = CCam::MODE_FIXED; + m_bPlayerIsInGarage = true; + } + }else{ + if(m_bPlayerIsInGarage){ + m_bJustCameOutOfGarage = true; + m_bPlayerIsInGarage = false; + } + ReqMode = CCam::MODE_FOLLOWPED; + } + }else{ + if(m_bPlayerIsInGarage){ + m_bJustCameOutOfGarage = true; + m_bPlayerIsInGarage = false; + } + m_bGarageFixedCamPositionSet = false; + } + + // Fallen into water + if(Cams[ActiveCam].IsTargetInWater(Cams[ActiveCam].Source) && + Cams[ActiveCam].CamTargetEntity->IsPed()) + ReqMode = CCam::MODE_PLAYER_FALLEN_WATER; + + // Set top down + if(PedZoomIndicator == CAM_ZOOM_TOPDOWN && + !CCullZones::Cam1stPersonForPlayer() && + !CCullZones::CamNoRain() && + !CCullZones::PlayerNoRain() && + !m_bFirstPersonBeingUsed && + !m_bPlayerIsInGarage) + ReqMode = CCam::MODE_TOP_DOWN_PED; + + // Weapon mode + if(!CPad::GetPad(0)->GetTarget() && PlayerWeaponMode.Mode != CCam::MODE_HELICANNON_1STPERSON) + ClearPlayerWeaponMode(); + if(m_PlayerMode.Mode != CCam::MODE_NONE) + ReqMode = m_PlayerMode.Mode; + if(PlayerWeaponMode.Mode != CCam::MODE_NONE && !stairs){ + if(PlayerWeaponMode.Mode == CCam::MODE_SNIPER || + PlayerWeaponMode.Mode == CCam::MODE_ROCKETLAUNCHER || + PlayerWeaponMode.Mode == CCam::MODE_M16_1STPERSON || + PlayerWeaponMode.Mode == CCam::MODE_HELICANNON_1STPERSON || + Cams[ActiveCam].GetWeaponFirstPersonOn()){ + // First person weapon mode + if(PLAYER->GetPedState() == PED_SEEK_CAR){ + if(ReqMode == CCam::MODE_TOP_DOWN_PED || Cams[ActiveCam].GetWeaponFirstPersonOn()) + ReqMode = PlayerWeaponMode.Mode; + else + ReqMode = CCam::MODE_FOLLOWPED; + }else + ReqMode = PlayerWeaponMode.Mode; + }else if(ReqMode != CCam::MODE_TOP_DOWN_PED){ + // Syphon mode + float playerTargetDist; + float deadPedDist = 4.0f; + static float alivePedDist = 2.0f; // original name lost + float pedDist; // actually only used on dead target + bool targetDead = false; + float camAngle, targetAngle; + CVector playerToTarget = m_cvecAimingTargetCoors - pTargetEntity->GetPosition(); + CVector playerToCam = Cams[ActiveCam].Source - pTargetEntity->GetPosition(); + + if(PedZoomIndicator == CAM_ZOOM_1) + deadPedDist = 2.25f; + if(FindPlayerPed()->m_pPointGunAt){ + // BUG: this need not be a ped! + if(((CPed*)FindPlayerPed()->m_pPointGunAt)->DyingOrDead()){ + targetDead = true; + pedDist = deadPedDist; + }else + pedDist = alivePedDist; + playerTargetDist = playerToTarget.Magnitude2D(); + camAngle = CGeneral::GetATanOfXY(playerToCam.x, playerToCam.y); + targetAngle = CGeneral::GetATanOfXY(playerToTarget.x, playerToTarget.y); + ReqMode = PlayerWeaponMode.Mode; + + // Check whether to start aiming in crim-in-front mode + if(Cams[ActiveCam].Mode != CCam::MODE_SYPHON){ + float angleDiff = camAngle - targetAngle; + while(angleDiff >= PI) angleDiff -= 2*PI; + while(angleDiff < -PI) angleDiff += 2*PI; + if(Abs(angleDiff) < HALFPI && playerTargetDist < 3.5f && playerToTarget.z > -1.0f) + ReqMode = CCam::MODE_SYPHON_CRIM_IN_FRONT; + } + + // Check whether to go to special fixed mode + float fixedModeDist = 0.0f; + if((ReqMode == CCam::MODE_SYPHON_CRIM_IN_FRONT || ReqMode == CCam::MODE_SYPHON) && + (m_uiTransitionState == 0 || Cams[ActiveCam].Mode == CCam::MODE_SPECIAL_FIXED_FOR_SYPHON) && + playerTargetDist < pedDist && targetDead){ + if(ReqMode == CCam::MODE_SYPHON_CRIM_IN_FRONT) + fixedModeDist = 5.0f; + else + fixedModeDist = 3.0f; + ReqMode = CCam::MODE_SPECIAL_FIXED_FOR_SYPHON; + } + if(ReqMode == CCam::MODE_SPECIAL_FIXED_FOR_SYPHON){ + if(!PlaceForFixedWhenSniperFound){ + // Find position + CEntity *entity; + CColPoint colPoint; + CVector fixedPos = pTargetEntity->GetPosition(); + fixedPos.x += fixedModeDist*Cos(camAngle); + fixedPos.y += fixedModeDist*Sin(camAngle); + fixedPos.z += 1.15f; + if(CWorld::ProcessLineOfSight(pTargetEntity->GetPosition(), fixedPos, colPoint, entity, true, false, false, true, false, true, true)) + SetCamPositionForFixedMode(colPoint.point, CVector(0.0f, 0.0f, 0.0f)); + else + SetCamPositionForFixedMode(fixedPos, CVector(0.0f, 0.0f, 0.0f)); + PlaceForFixedWhenSniperFound = true; + } + }else + PlaceForFixedWhenSniperFound = false; + } + } + } + } + } + + m_bIdleOn = false; + + if(DebugCamMode) + ReqMode = DebugCamMode; + + + // Process arrested player + static int ThePickedArrestMode; + static int LastPedState; + bool startArrestCam = false; + + if(LastPedState != PED_ARRESTED && PLAYER->GetPedState() == PED_ARRESTED){ + if(CarZoomIndicator != CAM_ZOOM_1STPRS && pTargetEntity->IsVehicle()) + startArrestCam = true; + }else + startArrestCam = false; + LastPedState = PLAYER->GetPedState(); + if(startArrestCam){ + if(m_uiTransitionState) + ReqMode = Cams[ActiveCam].Mode; + else{ + bool valid; + if(pTargetEntity->IsPed()){ + // How can this happen if arrest cam is only done in cars? + Cams[(ActiveCam+1)%2].ResetStatics = true; + valid = Cams[(ActiveCam+1)%2].ProcessArrestCamOne(); + ReqMode = CCam::MODE_ARRESTCAM_ONE; + }else{ + Cams[(ActiveCam+1)%2].ResetStatics = true; + valid = Cams[(ActiveCam+1)%2].ProcessArrestCamTwo(); + ReqMode = CCam::MODE_ARRESTCAM_TWO; + } + if(!valid) + ReqMode = Cams[ActiveCam].Mode; + } + } + ThePickedArrestMode = ReqMode; + if(PLAYER->GetPedState() == PED_ARRESTED) + ReqMode = ThePickedArrestMode; // this is rather useless... + + // Process dead player + if(PLAYER->GetPedState() == PED_DEAD){ + if(Cams[ActiveCam].Mode == CCam::MODE_PED_DEAD_BABY) + ReqMode = CCam::MODE_PED_DEAD_BABY; + else{ + bool foundRoof; + CVector pos = FindPlayerPed()->GetPosition(); + CWorld::FindRoofZFor3DCoord(pos.x, pos.y, pos.z, &foundRoof); + if(!foundRoof) + ReqMode = CCam::MODE_PED_DEAD_BABY; + } + } + + // Restore with a jump cut + if(m_bRestoreByJumpCut){ + if(ReqMode != CCam::MODE_FOLLOWPED && + ReqMode != CCam::MODE_M16_1STPERSON && + ReqMode != CCam::MODE_SNIPER && + ReqMode != CCam::MODE_ROCKETLAUNCHER || + !m_bUseMouse3rdPerson) + SetCameraDirectlyBehindForFollowPed_CamOnAString(); + + ReqMode = m_iModeToGoTo; + Cams[ActiveCam].Mode = ReqMode; + m_bJust_Switched = true; + Cams[ActiveCam].ResetStatics = true; + Cams[ActiveCam].m_cvecCamFixedModeVector = m_vecFixedModeVector; + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + Cams[ActiveCam].m_cvecCamFixedModeSource = m_vecFixedModeSource; + Cams[ActiveCam].m_cvecCamFixedModeUpOffSet = m_vecFixedModeUpOffSet; + Cams[ActiveCam].m_bCamLookingAtVector = false; + Cams[ActiveCam].m_vecLastAboveWaterCamPosition = Cams[(ActiveCam+1)%2].m_vecLastAboveWaterCamPosition; + m_bRestoreByJumpCut = false; + Cams[ActiveCam].ResetStatics = true; + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + CarZoomValueSmooth = CarZoomValue; + m_fPedZoomValueSmooth = m_fPedZoomValue; + m_uiTransitionState = 0; + m_vecDoingSpecialInterPolation = false; + } + + if(gbModelViewer) + ReqMode = CCam::MODE_MODELVIEW; + + // Turn on Obbe's cam + bool canUseObbeCam = true; + if(pTargetEntity){ + if(pTargetEntity->IsVehicle()){ + if(CarZoomIndicator == CAM_ZOOM_CINEMATIC) + m_bObbeCinematicCarCamOn = true; + }else{ + if(PedZoomIndicator == CAM_ZOOM_CINEMATIC) + m_bObbeCinematicPedCamOn = true; + } + } + if(m_bTargetJustBeenOnTrain || + ReqMode == CCam::MODE_SYPHON || ReqMode == CCam::MODE_SYPHON_CRIM_IN_FRONT || ReqMode == CCam::MODE_SPECIAL_FIXED_FOR_SYPHON || + ReqMode == CCam::MODE_PED_DEAD_BABY || ReqMode == CCam::MODE_ARRESTCAM_ONE || ReqMode == CCam::MODE_ARRESTCAM_TWO || + ReqMode == CCam::MODE_FIGHT_CAM || ReqMode == CCam::MODE_PLAYER_FALLEN_WATER || + ReqMode == CCam::MODE_SNIPER || ReqMode == CCam::MODE_ROCKETLAUNCHER || ReqMode == CCam::MODE_M16_1STPERSON || + ReqMode == CCam::MODE_SNIPER_RUNABOUT || ReqMode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + ReqMode == CCam::MODE_1STPERSON_RUNABOUT || ReqMode == CCam::MODE_M16_1STPERSON_RUNABOUT || + ReqMode == CCam::MODE_FIGHT_CAM_RUNABOUT || ReqMode == CCam::MODE_HELICANNON_1STPERSON || + WhoIsInControlOfTheCamera == CAMCONTROL_SCRIPT || + m_bJustCameOutOfGarage || m_bPlayerIsInGarage) + canUseObbeCam = false; + + if(m_bObbeCinematicPedCamOn && canUseObbeCam) + ProcessObbeCinemaCameraPed(); + else if(m_bObbeCinematicCarCamOn && canUseObbeCam) + ProcessObbeCinemaCameraCar(); + else{ + if(m_bPlayerIsInGarage && m_bObbeCinematicCarCamOn) + switchByJumpCut = true; + canUseObbeCam = false; + DontProcessObbeCinemaCamera(); + } + + // Start the transition or do a jump cut + if(m_bLookingAtPlayer){ + // Going into top down modes normally needs a jump cut (but see below) + if(ReqMode == CCam::MODE_TOPDOWN || ReqMode == CCam::MODE_1STPERSON || ReqMode == CCam::MODE_TOP_DOWN_PED){ + switchByJumpCut = true; + } + // Going from top down to vehicle + else if(ReqMode == CCam::MODE_CAM_ON_A_STRING || ReqMode == CCam::MODE_BEHINDBOAT){ + if(Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED) + switchByJumpCut = true; + }else if(ReqMode == CCam::MODE_FIXED){ + if(Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN) + switchByJumpCut = true; + } + + // Top down modes can interpolate between each other + if(ReqMode == CCam::MODE_TOPDOWN){ + if(Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED || Cams[ActiveCam].Mode == CCam::MODE_PED_DEAD_BABY) + switchByJumpCut = false; + }else if(ReqMode == CCam::MODE_TOP_DOWN_PED){ + if(Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN || Cams[ActiveCam].Mode == CCam::MODE_PED_DEAD_BABY) + switchByJumpCut = false; + } + + if(ReqMode == CCam::MODE_1STPERSON || ReqMode == CCam::MODE_M16_1STPERSON || + ReqMode == CCam::MODE_SNIPER || ReqMode == CCam::MODE_ROCKETLAUNCHER || + ReqMode == CCam::MODE_SNIPER_RUNABOUT || ReqMode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + ReqMode == CCam::MODE_1STPERSON_RUNABOUT || ReqMode == CCam::MODE_M16_1STPERSON_RUNABOUT || + ReqMode == CCam::MODE_FIGHT_CAM_RUNABOUT || + ReqMode == CCam::MODE_HELICANNON_1STPERSON || + ReqMode == CCam::MODE_ARRESTCAM_ONE || ReqMode == CCam::MODE_ARRESTCAM_TWO){ + // Going into any 1st person mode is a jump cut + if(pTargetEntity->IsPed()) + switchByJumpCut = true; + }else if(ReqMode == CCam::MODE_FIXED && m_bPlayerIsInGarage){ + // Going from 1st peron mode into garage + if(Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_HELICANNON_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED || + stairs || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON_RUNABOUT){ + if(pTargetEntity && pTargetEntity->IsVehicle()) + switchByJumpCut = true; + } + }else if(ReqMode == CCam::MODE_FOLLOWPED){ + if(Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER || + Cams[ActiveCam].Mode == CCam::MODE_ARRESTCAM_ONE || + Cams[ActiveCam].Mode == CCam::MODE_ARRESTCAM_TWO || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_PED_DEAD_BABY || + Cams[ActiveCam].Mode == CCam::MODE_PILLOWS_PAPS || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_HELICANNON_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_TOPDOWN || + Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ + if(!m_bJustCameOutOfGarage){ + if(Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_HELICANNON_1STPERSON){ + float angle = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) - HALFPI; + ((CPed*)pTargetEntity)->m_fRotationCur = angle; + ((CPed*)pTargetEntity)->m_fRotationDest = angle; + } + m_bUseTransitionBeta = true; + switchByJumpCut = true; + if(Cams[ActiveCam].Mode == CCam::MODE_TOP_DOWN_PED){ + CVector front = Cams[ActiveCam].Source - FindPlayerPed()->GetPosition(); + front.z = 0.0f; + front.Normalise(); +#ifdef FIX_BUGS + // this is almost as bad as the bugged code + if(front.x == 0.001f && front.y == 0.001f) + front.y = 1.0f; +#else + // someone used = instead of == in the above check by accident + front.x = 0.001f; + front.y = 1.0f; +#endif + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(front.x, front.y); + }else + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) + PI; + } + } + }else if(ReqMode == CCam::MODE_FIGHT_CAM){ + if(Cams[ActiveCam].Mode == CCam::MODE_1STPERSON) + switchByJumpCut = true; + } + + if(ReqMode != Cams[ActiveCam].Mode && Cams[ActiveCam].CamTargetEntity == nil) + switchByJumpCut = true; + if(m_bPlayerIsInGarage && pToGarageWeAreIn){ + if(pToGarageWeAreIn->m_eGarageType == GARAGE_BOMBSHOP1 || + pToGarageWeAreIn->m_eGarageType == GARAGE_BOMBSHOP2 || + pToGarageWeAreIn->m_eGarageType == GARAGE_BOMBSHOP3){ + if(pTargetEntity->IsVehicle() && pTargetEntity->GetModelIndex() == MI_MRWHOOP && + ReqMode != Cams[ActiveCam].Mode) + switchByJumpCut = true; + } + } + if(CSceneEdit::m_bEditOn) + ReqMode = CCam::MODE_EDITOR; + + if((m_uiTransitionState == 0 || switchByJumpCut) && ReqMode != Cams[ActiveCam].Mode){ + if(switchByJumpCut){ + if(!m_bPlayerIsInGarage || m_bJustCameOutOfGarage){ + if(ReqMode != CCam::MODE_FOLLOWPED && + ReqMode != CCam::MODE_M16_1STPERSON && + ReqMode != CCam::MODE_SNIPER && + ReqMode != CCam::MODE_ROCKETLAUNCHER || + !m_bUseMouse3rdPerson) + SetCameraDirectlyBehindForFollowPed_CamOnAString(); + } + Cams[ActiveCam].Mode = ReqMode; + m_bJust_Switched = true; + Cams[ActiveCam].m_cvecCamFixedModeVector = m_vecFixedModeVector; + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + Cams[ActiveCam].m_cvecCamFixedModeSource = m_vecFixedModeSource; + Cams[ActiveCam].m_cvecCamFixedModeUpOffSet = m_vecFixedModeUpOffSet; + Cams[ActiveCam].m_bCamLookingAtVector = m_bLookingAtVector; + Cams[ActiveCam].m_vecLastAboveWaterCamPosition = Cams[(ActiveCam+1)%2].m_vecLastAboveWaterCamPosition; + CarZoomValueSmooth = CarZoomValue; + m_fPedZoomValueSmooth = m_fPedZoomValue; + m_uiTransitionState = 0; + m_vecDoingSpecialInterPolation = false; + m_bStartInterScript = false; + Cams[ActiveCam].ResetStatics = true; + + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + }else if(!m_bWaitForInterpolToFinish){ + StartTransition(ReqMode); + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + } + }else if(m_uiTransitionState != 0 && ReqMode != Cams[ActiveCam].Mode){ + bool startTransition = true; + + if(ReqMode == CCam::MODE_FIGHT_CAM || Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM) + startTransition = false; + if(ReqMode == CCam::MODE_FOLLOWPED && Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM) + startTransition = false; + + if(!m_bWaitForInterpolToFinish && m_bLookingAtPlayer && m_uiTransitionState != 0){ + CVector playerDist; + playerDist.x = FindPlayerPed()->GetPosition().x - GetPosition().x; + playerDist.y = FindPlayerPed()->GetPosition().y - GetPosition().y; + playerDist.z = FindPlayerPed()->GetPosition().z - GetPosition().z; + // if player is too far away, keep interpolating and don't transition + if(pTargetEntity && pTargetEntity->IsPed()){ + if(playerDist.Magnitude() > 17.5f && + (ReqMode == CCam::MODE_SYPHON || ReqMode == CCam::MODE_SYPHON_CRIM_IN_FRONT)) + m_bWaitForInterpolToFinish = true; + } + } + if(m_bWaitForInterpolToFinish) + startTransition = false; + + if(startTransition){ + StartTransitionWhenNotFinishedInter(ReqMode); + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + } + }else if(ReqMode == CCam::MODE_FIXED && pTargetEntity != Cams[ActiveCam].CamTargetEntity && m_bPlayerIsInGarage){ + if(m_uiTransitionState != 0) + StartTransitionWhenNotFinishedInter(ReqMode); + else + StartTransition(ReqMode); + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + } + }else{ + // not following player + if(m_uiTransitionState == 0 && m_bStartInterScript && m_iTypeOfSwitch == INTERPOLATION){ + ReqMode = m_iModeToGoTo; + StartTransition(ReqMode); + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + }else if(m_uiTransitionState != 0 && m_bStartInterScript && m_iTypeOfSwitch == INTERPOLATION){ + ReqMode = m_iModeToGoTo; + StartTransitionWhenNotFinishedInter(ReqMode); + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + }else if(m_bStartInterScript && m_iTypeOfSwitch == JUMP_CUT){ + m_uiTransitionState = 0; + m_vecDoingSpecialInterPolation = false; + Cams[ActiveCam].Mode = m_iModeToGoTo; + m_bJust_Switched = true; + Cams[ActiveCam].ResetStatics = true; + Cams[ActiveCam].m_cvecCamFixedModeVector = m_vecFixedModeVector; + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + Cams[ActiveCam].m_cvecCamFixedModeSource = m_vecFixedModeSource; + Cams[ActiveCam].m_cvecCamFixedModeUpOffSet = m_vecFixedModeUpOffSet; + Cams[ActiveCam].m_bCamLookingAtVector = m_bLookingAtVector; + Cams[ActiveCam].m_vecLastAboveWaterCamPosition = Cams[(ActiveCam+1)%2].m_vecLastAboveWaterCamPosition; + m_bJust_Switched = true; + pTargetEntity->RegisterReference(&pTargetEntity); + Cams[ActiveCam].CamTargetEntity->RegisterReference(&Cams[ActiveCam].CamTargetEntity); + CarZoomValueSmooth = CarZoomValue; + m_fPedZoomValueSmooth = m_fPedZoomValue; + } + } + + m_bStartInterScript = false; + + if(Cams[ActiveCam].CamTargetEntity == nil) + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + + // Ped visibility + if((Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER) && pTargetEntity->IsPed() || + Cams[ActiveCam].Mode == CCam::MODE_FLYBY) + FindPlayerPed()->bIsVisible = false; + else + FindPlayerPed()->bIsVisible = true; + + if(!canUseObbeCam && WhoIsInControlOfTheCamera == CAMCONTROL_OBBE) + Restore(); +} + +// What a mess! +void +CCamera::UpdateTargetEntity(void) +{ + bool enteringCar = false; // not on PS2 but only used as && !enteringCar so we can keep it + bool obbeCam = false; + + if(WhoIsInControlOfTheCamera == CAMCONTROL_OBBE){ + obbeCam = true; + if(m_iModeObbeCamIsInForCar == OBBE_COPCAR_WHEEL || m_iModeObbeCamIsInForCar == OBBE_COPCAR){ + if(FindPlayerPed()->GetPedState() != PED_ARRESTED) + obbeCam = false; + if(FindPlayerVehicle() == nil) + pTargetEntity = FindPlayerPed(); + } + } + + if((m_bLookingAtPlayer || obbeCam) && m_uiTransitionState == 0 || + pTargetEntity == nil || + m_bTargetJustBeenOnTrain){ + if(FindPlayerVehicle()) + pTargetEntity = FindPlayerVehicle(); + else{ + pTargetEntity = FindPlayerPed(); +#ifndef GTA_PS2_STUFF + // this keeps the camera on the player while entering cars + if(PLAYER->GetPedState() == PED_ENTER_CAR || + PLAYER->GetPedState() == PED_CARJACK || + PLAYER->GetPedState() == PED_OPEN_DOOR) + enteringCar = true; + + if(!enteringCar) + if(Cams[ActiveCam].CamTargetEntity != pTargetEntity) + Cams[ActiveCam].CamTargetEntity = pTargetEntity; +#endif + } + + bool cantOpen = true; + if(PLAYER && + PLAYER->m_pMyVehicle && + PLAYER->m_pMyVehicle->CanPedOpenLocks(PLAYER)) + cantOpen = false; + + if(PLAYER->GetPedState() == PED_ENTER_CAR && !cantOpen){ + if(!enteringCar && CarZoomIndicator != 0.0f){ + pTargetEntity = PLAYER->m_pMyVehicle; + if(PLAYER->m_pMyVehicle == nil) + pTargetEntity = PLAYER; + } + } + + if((PLAYER->GetPedState() == PED_CARJACK || PLAYER->GetPedState() == PED_OPEN_DOOR) && !cantOpen){ + if(!enteringCar && CarZoomIndicator != 0.0f) +#ifdef GTA_PS2_STUFF +// dunno if this has any amazing effects + { +#endif + pTargetEntity = PLAYER->m_pMyVehicle; + if(PLAYER->m_pMyVehicle == nil) + pTargetEntity = PLAYER; +#ifdef GTA_PS2_STUFF + } +#endif + } + + if(PLAYER->GetPedState() == PED_EXIT_CAR) + pTargetEntity = FindPlayerPed(); + if(PLAYER->GetPedState() == PED_DRAG_FROM_CAR) + pTargetEntity = FindPlayerPed(); + if(pTargetEntity->IsVehicle() && CarZoomIndicator != 0.0f && FindPlayerPed()->GetPedState() == PED_ARRESTED) + pTargetEntity = FindPlayerPed(); + } +} + +const float SOUND_DIST = 20.0f; + +void +CCamera::UpdateSoundDistances(void) +{ + CVector center, end; + CEntity *entity; + CColPoint colPoint; + float f; + int n; + + if((Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_HELICANNON_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER) && + pTargetEntity->IsPed()) + center = GetPosition() + 0.5f*GetForward(); + else + center = GetPosition() + 5.0f*GetForward(); + + // check up + n = CTimer::GetFrameCounter() % 12; + if(n == 0){ + SoundDistUpAsReadOld = SoundDistUpAsRead; + if(CWorld::ProcessVerticalLine(center, center.z+SOUND_DIST, colPoint, entity, true, false, false, false, true, false, nil)) + SoundDistUpAsRead = colPoint.point.z - center.z; + else + SoundDistUpAsRead = SOUND_DIST; + } + f = (n + 1) / 6.0f; + SoundDistUp = (1.0f-f)*SoundDistUpAsReadOld + f*SoundDistUpAsRead; + + // check left + n = (CTimer::GetFrameCounter()+2) % 12; + if(n == 0){ + SoundDistLeftAsReadOld = SoundDistLeftAsRead; + end = center + SOUND_DIST*GetRight(); + if(CWorld::ProcessLineOfSight(center, end, colPoint, entity, true, false, false, false, true, true, true)) + SoundDistLeftAsRead = (colPoint.point - center).Magnitude(); + else + SoundDistLeftAsRead = SOUND_DIST; + } + f = (n + 1) / 6.0f; + SoundDistLeft = (1.0f-f)*SoundDistLeftAsReadOld + f*SoundDistLeftAsRead; + + // check right + // end = center - SOUND_DIST*GetRight(); // useless + n = (CTimer::GetFrameCounter()+4) % 12; + if(n == 0){ + SoundDistRightAsReadOld = SoundDistRightAsRead; + end = center - SOUND_DIST*GetRight(); + if(CWorld::ProcessLineOfSight(center, end, colPoint, entity, true, false, false, false, true, true, true)) + SoundDistRightAsRead = (colPoint.point - center).Magnitude(); + else + SoundDistRightAsRead = SOUND_DIST; + } + f = (n + 1) / 6.0f; + SoundDistRight = (1.0f-f)*SoundDistRightAsReadOld + f*SoundDistRightAsRead; +} + +void +CCamera::InitialiseCameraForDebugMode(void) +{ + if(FindPlayerVehicle()) + Cams[2].Source = FindPlayerVehicle()->GetPosition(); + else if(FindPlayerPed()) + Cams[2].Source = FindPlayerPed()->GetPosition(); + Cams[2].Alpha = 0.0f; + Cams[2].Beta = 0.0f; + Cams[2].Mode = CCam::MODE_DEBUG; +} + +void +CCamera::CamShake(float strength, float x, float y, float z) +{ + CVector Dist = Cams[ActiveCam].Source - CVector(x, y, z); + // a bit complicated... + float dist2d = Sqrt(SQR(Dist.x) + SQR(Dist.y)); + float dist3d = Sqrt(SQR(dist2d) + SQR(Dist.z)); + if(dist3d > 100.0f) dist3d = 100.0f; + if(dist3d < 0.0f) dist3d = 0.0f; + float mult = 1.0f - dist3d/100.0f; + + float curForce = mult*(m_fCamShakeForce - (CTimer::GetTimeInMilliseconds() - m_uiCamShakeStart)/1000.0f); + strength = mult*strength; + if(clamp(curForce, 0.0f, 2.0f) < strength){ + m_fCamShakeForce = strength; + m_uiCamShakeStart = CTimer::GetTimeInMilliseconds(); + } +} + +// This seems to be CCamera::CamShake(float) on PS2 +void +CamShakeNoPos(CCamera *cam, float strength) +{ + float curForce = cam->m_fCamShakeForce - (CTimer::GetTimeInMilliseconds() - cam->m_uiCamShakeStart)/1000.0f; + if(clamp(curForce, 0.0f, 2.0f) < strength){ + cam->m_fCamShakeForce = strength; + cam->m_uiCamShakeStart = CTimer::GetTimeInMilliseconds(); + } +} + + + +void +CCamera::TakeControl(CEntity *target, int16 mode, int16 typeOfSwitch, int32 controller) +{ + bool doSwitch = true; + if(controller == CAMCONTROL_OBBE && WhoIsInControlOfTheCamera == CAMCONTROL_SCRIPT) + doSwitch = false; + if(doSwitch){ + WhoIsInControlOfTheCamera = controller; + if(target){ + if(mode == CCam::MODE_NONE){ + // Why are we checking the old entity? + if(pTargetEntity->IsPed()) + mode = CCam::MODE_FOLLOWPED; + else if(pTargetEntity->IsVehicle()) + mode = CCam::MODE_CAM_ON_A_STRING; + } + }else if(FindPlayerVehicle()) + target = FindPlayerVehicle(); + else + target = PLAYER; + + m_bLookingAtVector = false; + pTargetEntity = target; + m_iModeToGoTo = mode; + m_iTypeOfSwitch = typeOfSwitch; + m_bLookingAtPlayer = false; + m_bStartInterScript = true; + // FindPlayerPed(); // unused + } +} + +void +CCamera::TakeControlNoEntity(const CVector &position, int16 typeOfSwitch, int32 controller) +{ + bool doSwitch = true; + if(controller == CAMCONTROL_OBBE && WhoIsInControlOfTheCamera == CAMCONTROL_SCRIPT) + doSwitch = false; + if(doSwitch){ + WhoIsInControlOfTheCamera = controller; + m_bLookingAtVector = true; + m_bLookingAtPlayer = false; + m_iModeToGoTo = CCam::MODE_FIXED; + m_vecFixedModeVector = position; + m_iTypeOfSwitch = typeOfSwitch; + m_bStartInterScript = true; + } +} + +void +CCamera::TakeControlWithSpline(int16 typeOfSwitch) +{ + m_iModeToGoTo = CCam::MODE_FLYBY; + m_bLookingAtPlayer = false; + m_bLookingAtVector = false; + m_bcutsceneFinished = false; + m_iTypeOfSwitch = typeOfSwitch; + m_bStartInterScript = true; + + //FindPlayerPed(); // unused +}; + +void +CCamera::Restore(void) +{ + m_bLookingAtPlayer = true; + m_bLookingAtVector = false; + m_iTypeOfSwitch = INTERPOLATION; + m_bUseNearClipScript = false; + m_iModeObbeCamIsInForCar = OBBE_INVALID; + m_fPositionAlongSpline = 0.0; + m_bStartingSpline = false; + m_bScriptParametersSetForInterPol = false; + WhoIsInControlOfTheCamera = CAMCONTROL_GAME; + + if(FindPlayerVehicle()){ + m_iModeToGoTo = CCam::MODE_CAM_ON_A_STRING; + pTargetEntity = FindPlayerVehicle(); + }else{ + m_iModeToGoTo = CCam::MODE_FOLLOWPED; + pTargetEntity = PLAYER; + } + + if(PLAYER->GetPedState() == PED_ENTER_CAR || + PLAYER->GetPedState() == PED_CARJACK || + PLAYER->GetPedState() == PED_OPEN_DOOR){ + m_iModeToGoTo = CCam::MODE_CAM_ON_A_STRING; + pTargetEntity = PLAYER->m_pSeekTarget; + } + if(PLAYER->GetPedState() == PED_EXIT_CAR){ + m_iModeToGoTo = CCam::MODE_FOLLOWPED; + pTargetEntity = PLAYER; + } + + m_bUseScriptZoomValuePed = false; + m_bUseScriptZoomValueCar = false; + m_bStartInterScript = true; + m_bCameraJustRestored = true; +} + +void +CCamera::RestoreWithJumpCut(void) +{ + m_bRestoreByJumpCut = true; + m_bLookingAtPlayer = true; + m_bLookingAtVector = false; + m_iTypeOfSwitch = JUMP_CUT; + m_bUseNearClipScript = false; + m_iModeObbeCamIsInForCar = OBBE_INVALID; + m_fPositionAlongSpline = 0.0; + m_bStartingSpline = false; + m_bScriptParametersSetForInterPol = false; + WhoIsInControlOfTheCamera = CAMCONTROL_GAME; + m_bCameraJustRestored = true; + + if(FindPlayerVehicle()){ + m_iModeToGoTo = CCam::MODE_CAM_ON_A_STRING; + pTargetEntity = FindPlayerVehicle(); + }else{ + m_iModeToGoTo = CCam::MODE_FOLLOWPED; + pTargetEntity = PLAYER; + } + + if(PLAYER->GetPedState() == PED_ENTER_CAR || + PLAYER->GetPedState() == PED_CARJACK || + PLAYER->GetPedState() == PED_OPEN_DOOR){ + m_iModeToGoTo = CCam::MODE_CAM_ON_A_STRING; + pTargetEntity = PLAYER->m_pSeekTarget; + } + if(PLAYER->GetPedState() == PED_EXIT_CAR){ + m_iModeToGoTo = CCam::MODE_FOLLOWPED; + pTargetEntity = PLAYER; + } + + m_bUseScriptZoomValuePed = false; + m_bUseScriptZoomValueCar = false; +} + +void +CCamera::SetCamPositionForFixedMode(const CVector &Source, const CVector &UpOffSet) +{ + m_vecFixedModeSource = Source; + m_vecFixedModeUpOffSet = UpOffSet; +} + + + +/* + * On PS2 the transition happens between Cams[1] and Cams[2]. + * On PC the whole system has been changed. + */ +void +CCamera::StartTransition(int16 newMode) +{ + bool foo = false; + bool switchSyphonMode = false; + bool switchPedToCar = false; + bool switchPedMode = false; + bool switchFromFixed = false; + bool switch1stPersonToVehicle = false; + float betaOffset, targetBeta, camBeta, deltaBeta; + int door; + bool vehicleVertical; + +// missing on PS2 + m_bItsOkToLookJustAtThePlayer = false; + m_fFractionInterToStopMovingTarget = 0.25f; + m_fFractionInterToStopCatchUpTarget = 0.75f; + + if(Cams[ActiveCam].Mode == CCam::MODE_SYPHON_CRIM_IN_FRONT || + Cams[ActiveCam].Mode == CCam::MODE_FOLLOWPED || + Cams[ActiveCam].Mode == CCam::MODE_SYPHON || + Cams[ActiveCam].Mode == CCam::MODE_SPECIAL_FIXED_FOR_SYPHON){ + if(newMode == CCam::MODE_SYPHON_CRIM_IN_FRONT || + newMode == CCam::MODE_FOLLOWPED || + newMode == CCam::MODE_SYPHON || + newMode == CCam::MODE_SPECIAL_FIXED_FOR_SYPHON) + m_bItsOkToLookJustAtThePlayer = true; + if(newMode == CCam::MODE_CAM_ON_A_STRING) + switchPedToCar = true; + } +// + + if(Cams[ActiveCam].Mode == CCam::MODE_SYPHON_CRIM_IN_FRONT && newMode == CCam::MODE_SYPHON) + switchSyphonMode = true; + if(Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM && newMode == CCam::MODE_FOLLOWPED) + switchPedMode = true; + if(Cams[ActiveCam].Mode == CCam::MODE_FIXED) + switchFromFixed = true; + + m_bUseTransitionBeta = false; + + if((Cams[ActiveCam].Mode == CCam::MODE_SNIPER || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_SNIPER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_M16_1STPERSON_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_FIGHT_CAM_RUNABOUT || + Cams[ActiveCam].Mode == CCam::MODE_HELICANNON_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON_RUNABOUT) && + pTargetEntity->IsPed()){ + float angle = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) - HALFPI; + ((CPed*)pTargetEntity)->m_fRotationCur = angle; + ((CPed*)pTargetEntity)->m_fRotationDest = angle; + } + +/* // PS2 + ActiveCam = (ActiveCam+1)%2; + Cams[ActiveCam].Init(); + Cams[ActiveCam].Mode = newMode; + */ + + Cams[ActiveCam].m_cvecCamFixedModeVector = m_vecFixedModeVector; + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + Cams[ActiveCam].m_cvecCamFixedModeSource = m_vecFixedModeSource; + Cams[ActiveCam].m_cvecCamFixedModeUpOffSet = m_vecFixedModeUpOffSet; + Cams[ActiveCam].m_bCamLookingAtVector = m_bLookingAtVector; + + if(newMode == CCam::MODE_SNIPER || + newMode == CCam::MODE_ROCKETLAUNCHER || + newMode == CCam::MODE_M16_1STPERSON || + newMode == CCam::MODE_SNIPER_RUNABOUT || + newMode == CCam::MODE_ROCKETLAUNCHER_RUNABOUT || + newMode == CCam::MODE_1STPERSON_RUNABOUT || + newMode == CCam::MODE_M16_1STPERSON_RUNABOUT || + newMode == CCam::MODE_FIGHT_CAM_RUNABOUT || + newMode == CCam::MODE_HELICANNON_1STPERSON) + Cams[ActiveCam].Alpha = 0.0f; + + // PS2 also copies values to ActiveCam here + switch(Cams[ActiveCam].Mode) + case CCam::MODE_SNIPER_RUNABOUT: + case CCam::MODE_ROCKETLAUNCHER_RUNABOUT: + case CCam::MODE_1STPERSON_RUNABOUT: + case CCam::MODE_M16_1STPERSON_RUNABOUT: + case CCam::MODE_FIGHT_CAM_RUNABOUT: + if(newMode == CCam::MODE_CAM_ON_A_STRING || newMode == CCam::MODE_BEHINDBOAT) + switch1stPersonToVehicle = true; + + switch(newMode){ + case CCam::MODE_BEHINDCAR: + Cams[ActiveCam].BetaSpeed = 0.0f; + break; + + case CCam::MODE_FOLLOWPED: + // Getting out of vehicle normally + betaOffset = DEGTORAD(55.0f); + if(m_bJustCameOutOfGarage){ + m_bUseTransitionBeta = true; +/* + // weird logic... + if(CMenuManager::m_ControlMethod == CONTROL_CLASSIC) + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) + PI; + else if(Cams[ActiveCam].Front.x != 0.0f && Cams[ActiveCam].Front.y != 0.0f) // && is wrong here + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) + PI; + else + Cams[ActiveCam].m_fTransitionBeta = 0.0f; +*/ + // this is better: + if(Cams[ActiveCam].Front.x != 0.0f || Cams[ActiveCam].Front.y != 0.0f) + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y) + PI; + else + Cams[ActiveCam].m_fTransitionBeta = 0.0f; + } + if(m_bTargetJustCameOffTrain) + m_bCamDirectlyInFront = true; + if(Cams[ActiveCam].Mode != CCam::MODE_CAM_ON_A_STRING) + break; + m_bUseTransitionBeta = true; + vehicleVertical = false; + if(((CPed*)pTargetEntity)->m_carInObjective && + ((CPed*)pTargetEntity)->m_carInObjective->GetForward().x == 0.0f && + ((CPed*)pTargetEntity)->m_carInObjective->GetForward().y == 0.0f) + vehicleVertical = true; + if(vehicleVertical){ + Cams[ActiveCam].m_fTransitionBeta = 0.0f; + break; + } + camBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + if(((CPed*)pTargetEntity)->m_carInObjective) + targetBeta = CGeneral::GetATanOfXY(((CPed*)pTargetEntity)->m_carInObjective->GetForward().x, ((CPed*)pTargetEntity)->m_carInObjective->GetForward().y); + else + targetBeta = camBeta; + deltaBeta = targetBeta - camBeta; + while(deltaBeta >= PI) deltaBeta -= 2*PI; + while(deltaBeta < -PI) deltaBeta += 2*PI; + deltaBeta = Abs(deltaBeta); + + door = FindPlayerPed()->m_vehEnterType; + if(deltaBeta > HALFPI){ + if(((CPed*)pTargetEntity)->m_carInObjective){ + if(((CPed*)pTargetEntity)->m_carInObjective->IsUpsideDown()){ + if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) + betaOffset = -DEGTORAD(95.0f); + }else{ + if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = -DEGTORAD(95.0f); + } + } + Cams[ActiveCam].m_fTransitionBeta = targetBeta + betaOffset; + }else{ + if(((CPed*)pTargetEntity)->m_carInObjective){ + if(((CPed*)pTargetEntity)->m_carInObjective->IsUpsideDown()){ + if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = -DEGTORAD(55.0f); + else if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) + betaOffset = DEGTORAD(95.0f); + }else{ + if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) + betaOffset = -DEGTORAD(55.0f); + else if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = DEGTORAD(95.0f); + } + } + Cams[ActiveCam].m_fTransitionBeta = targetBeta + betaOffset + PI; + } + break; + + case CCam::MODE_SNIPER: + case CCam::MODE_ROCKETLAUNCHER: + case CCam::MODE_M16_1STPERSON: + case CCam::MODE_SNIPER_RUNABOUT: + case CCam::MODE_ROCKETLAUNCHER_RUNABOUT: + case CCam::MODE_1STPERSON_RUNABOUT: + case CCam::MODE_M16_1STPERSON_RUNABOUT: + case CCam::MODE_FIGHT_CAM_RUNABOUT: + case CCam::MODE_HELICANNON_1STPERSON: + if(FindPlayerVehicle()) + Cams[ActiveCam].Beta = Atan2(FindPlayerVehicle()->GetForward().x, FindPlayerVehicle()->GetForward().y); + else + Cams[ActiveCam].Beta = Atan2(PLAYER->GetForward().x, PLAYER->GetForward().y); + break; + + case CCam::MODE_SYPHON: + Cams[ActiveCam].Alpha = 0.0f; + Cams[ActiveCam].AlphaSpeed = 0.0f; + break; + + case CCam::MODE_CAM_ON_A_STRING: + // Get into vehicle + betaOffset = DEGTORAD(57.0f); + if(!m_bLookingAtPlayer || m_bJustCameOutOfGarage) + break; + m_bUseTransitionBeta = true; + targetBeta = CGeneral::GetATanOfXY(pTargetEntity->GetForward().x, pTargetEntity->GetForward().y); + camBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + deltaBeta = targetBeta - camBeta; + while(deltaBeta >= PI) deltaBeta -= 2*PI; + while(deltaBeta < -PI) deltaBeta += 2*PI; + deltaBeta = Abs(deltaBeta); + // switchFromFixed logic again here, skipped + if(switchFromFixed){ + Cams[ActiveCam].m_fTransitionBeta = CGeneral::GetATanOfXY(Cams[ActiveCam].Front.x, Cams[ActiveCam].Front.y); + break; + } + + door = FindPlayerPed()->m_vehEnterType; + if(deltaBeta > HALFPI){ + if(((CVehicle*)pTargetEntity)->IsUpsideDown()){ + if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) // BUG: game checks LF twice + betaOffset = -DEGTORAD(57.0f); + }else{ + if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = -DEGTORAD(57.0f); + } + Cams[ActiveCam].m_fTransitionBeta = targetBeta + betaOffset + PI; + }else{ + if(((CVehicle*)pTargetEntity)->IsUpsideDown()){ + if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = -DEGTORAD(57.0f); + else if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) + betaOffset = DEGTORAD(57.0f); + }else{ + if(door == CAR_DOOR_LF || door == CAR_DOOR_LR) + betaOffset = -DEGTORAD(57.0f); + else if(door == CAR_DOOR_RF || door == CAR_DOOR_RR) + betaOffset = DEGTORAD(57.0f); + } + Cams[ActiveCam].m_fTransitionBeta = targetBeta + betaOffset; + } + break; + + case CCam::MODE_BEHINDBOAT: + Cams[ActiveCam].BetaSpeed = 0.0f; + break; + + case CCam::MODE_PED_DEAD_BABY: + Cams[ActiveCam].Alpha = DEGTORAD(15.0f); + break; + + case CCam::MODE_FIGHT_CAM: + Cams[ActiveCam].Beta = 0.0f; + Cams[ActiveCam].BetaSpeed = 0.0f; + Cams[ActiveCam].Alpha = 0.0f; + Cams[ActiveCam].AlphaSpeed = 0.0f; + break; + } + + Cams[ActiveCam].Init(); + Cams[ActiveCam].Mode = newMode; + + m_uiTransitionDuration = 1350; + if(switchSyphonMode) + m_uiTransitionDuration = 1800; + else if(switchPedMode) + m_uiTransitionDuration = 750; +// not on PS2 + else if(switchPedToCar){ + m_fFractionInterToStopMovingTarget = 0.2f; + m_fFractionInterToStopCatchUpTarget = 0.8f; + m_uiTransitionDuration = 950; + }else if(switchFromFixed){ + m_fFractionInterToStopMovingTarget = 0.05f; + m_fFractionInterToStopCatchUpTarget = 0.95f; + }else if(switch1stPersonToVehicle){ + m_fFractionInterToStopMovingTarget = 0.0f; + m_fFractionInterToStopCatchUpTarget = 1.0f; + m_uiTransitionDuration = 1; + }else + m_uiTransitionDuration = 1350; // already set above +// + m_uiTransitionState = 1; + m_uiTimeTransitionStart = CTimer::GetTimeInMilliseconds(); + m_uiTransitionJUSTStarted = 1; +// PS2 returns here + if(m_vecDoingSpecialInterPolation){ + m_cvecStartingSourceForInterPol = SourceDuringInter; + m_cvecStartingTargetForInterPol = TargetDuringInter; + m_cvecStartingUpForInterPol = UpDuringInter; + m_fStartingAlphaForInterPol = m_fAlphaDuringInterPol; + m_fStartingBetaForInterPol = m_fBetaDuringInterPol; + }else{ + m_cvecStartingSourceForInterPol = Cams[ActiveCam].Source; + m_cvecStartingTargetForInterPol = Cams[ActiveCam].m_cvecTargetCoorsForFudgeInter; + m_cvecStartingUpForInterPol = Cams[ActiveCam].Up; + m_fStartingAlphaForInterPol = Cams[ActiveCam].m_fTrueAlpha; + m_fStartingBetaForInterPol = Cams[ActiveCam].m_fTrueBeta; + } + Cams[ActiveCam].m_bCamLookingAtVector = m_bLookingAtVector; + Cams[ActiveCam].m_cvecCamFixedModeVector = m_vecFixedModeVector; + Cams[ActiveCam].m_cvecCamFixedModeSource = m_vecFixedModeSource; + Cams[ActiveCam].m_cvecCamFixedModeUpOffSet = m_vecFixedModeUpOffSet; + Cams[ActiveCam].Mode = newMode; // already done above + Cams[ActiveCam].CamTargetEntity = pTargetEntity; + m_uiTransitionState = 1; // these three already done above + m_uiTimeTransitionStart = CTimer::GetTimeInMilliseconds(); + m_uiTransitionJUSTStarted = 1; + m_fStartingFOVForInterPol = Cams[ActiveCam].FOV; + m_cvecSourceSpeedAtStartInter = Cams[ActiveCam].m_cvecSourceSpeedOverOneFrame; + m_cvecTargetSpeedAtStartInter = Cams[ActiveCam].m_cvecTargetSpeedOverOneFrame; + m_cvecUpSpeedAtStartInter = Cams[ActiveCam].m_cvecUpOverOneFrame; + m_fAlphaSpeedAtStartInter = Cams[ActiveCam].m_fAlphaSpeedOverOneFrame; + m_fBetaSpeedAtStartInter = Cams[ActiveCam].m_fBetaSpeedOverOneFrame; + m_fFOVSpeedAtStartInter = Cams[ActiveCam].m_fFovSpeedOverOneFrame; + Cams[ActiveCam].ResetStatics = true; + if(!m_bLookingAtPlayer && m_bScriptParametersSetForInterPol){ + m_fFractionInterToStopMovingTarget = m_fScriptPercentageInterToStopMoving; + m_fFractionInterToStopCatchUpTarget = m_fScriptPercentageInterToCatchUp; + m_uiTransitionDuration = m_fScriptTimeForInterPolation; + } +} + +void +CCamera::StartTransitionWhenNotFinishedInter(int16 mode) +{ + m_vecDoingSpecialInterPolation = true; + StartTransition(mode); +} + +void +CCamera::StoreValuesDuringInterPol(CVector &source, CVector &target, CVector &up, float &FOV) +{ + SourceDuringInter = source; + TargetDuringInter = target; + UpDuringInter = up; + FOVDuringInter = FOV; + CVector Dist = source - TargetDuringInter; + float DistOnGround = Dist.Magnitude2D(); + m_fBetaDuringInterPol = CGeneral::GetATanOfXY(Dist.x, Dist.y); + m_fAlphaDuringInterPol = CGeneral::GetATanOfXY(DistOnGround, Dist.z); +} + + + +void +CCamera::SetWideScreenOn(void) +{ + m_WideScreenOn = true; +} + +void +CCamera::SetWideScreenOff(void) +{ + m_bWantsToSwitchWidescreenOff = m_WideScreenOn; +} + +void +CCamera::ProcessWideScreenOn(void) +{ + if(m_bWantsToSwitchWidescreenOff){ + m_bWantsToSwitchWidescreenOff = false; + m_WideScreenOn = false; + m_ScreenReductionPercentage = 0.0f; + m_fFOV_Wide_Screen = 0.0f; + m_fWideScreenReductionAmount = 0.0f; + }else{ + m_fFOV_Wide_Screen = 0.3f*Cams[ActiveCam].FOV; + m_fWideScreenReductionAmount = 1.0f; + m_ScreenReductionPercentage = 30.0f; + } +} + +void +CCamera::DrawBordersForWideScreen(void) +{ + if(m_BlurType == MBLUR_NONE || m_BlurType == MBLUR_NORMAL) + SetMotionBlurAlpha(80); + + CSprite2d::DrawRect( + CRect(0.0f, (SCREEN_HEIGHT/2) * m_ScreenReductionPercentage/100.0f - 8.0f, + SCREEN_WIDTH, 0.0f), + CRGBA(0, 0, 0, 255)); + + CSprite2d::DrawRect( + CRect(0.0f, SCREEN_HEIGHT, + SCREEN_WIDTH, SCREEN_HEIGHT - (SCREEN_HEIGHT/2) * m_ScreenReductionPercentage/100.0f - 8.0f), + CRGBA(0, 0, 0, 255)); +} + -WRAPPER void CCamera::CamShake(float strength, float x, float y, float z) { EAXJMP(0x46B200); } -WRAPPER void CCamera::DrawBordersForWideScreen(void) { EAXJMP(0x46B430); } -WRAPPER void CCamera::CalculateDerivedValues(void) { EAXJMP(0x46EEA0); } -WRAPPER void CCamera::Restore(void) { EAXJMP(0x46F990); } -WRAPPER void CamShakeNoPos(CCamera*, float) { EAXJMP(0x46B100); } -WRAPPER void CCamera::TakeControl(CEntity*, int16, int16, int32) { EAXJMP(0x471500); } -WRAPPER void CCamera::TakeControlNoEntity(const CVector&, int16, int32) { EAXJMP(0x4715B0); } -WRAPPER void CCamera::Init(void) { EAXJMP(0x46BAD0); } -WRAPPER void CCamera::Process(void) { EAXJMP(0x46D3F0); } -WRAPPER void CCamera::LoadPathSplines(int file) { EAXJMP(0x46D1D0); } -WRAPPER void CCamera::RestoreWithJumpCut(void) { EAXJMP(0x46FAE0); }; -WRAPPER void CCamera::SetPercentAlongCutScene(float) { EAXJMP(0x46FE20); }; -WRAPPER void CCamera::SetParametersForScriptInterpolation(float, float, int32) { EAXJMP(0x46FDE0); } bool -CCamera::GetFading() +CCamera::IsItTimeForNewcam(int32 obbeMode, int32 time) +{ + CVehicle *veh; + uint32 t = time; // no annoying compiler warnings + CVector fwd; + + if(obbeMode < 0) + return true; + switch(obbeMode){ + case OBBE_WHEEL: + veh = FindPlayerVehicle(); + if(veh == nil){ + if(CTimer::GetTimeInMilliseconds() > t+5000) + return true; + SetNearClipScript(0.6f); + return false; + } + if(veh->IsBoat() || veh->GetModelIndex() == MI_RHINO) + return true; + if(CWorld::GetIsLineOfSightClear(pTargetEntity->GetPosition(), Cams[ActiveCam].Source, true, false, false, false, false, false, false)){ + if(CTimer::GetTimeInMilliseconds() > t+5000) + return true; + SetNearClipScript(0.6f); + return false; + } + return true; + case OBBE_1: + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return true; + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 20.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + // too close + if(fwd.Magnitude() < 1.6f) + return true; + return false; + } + return true; + case OBBE_2: + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return true; + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + if(fwd.Magnitude() < 2.0f) + // very close, fix near clip + SetNearClipScript(max(fwd.Magnitude()*0.5f, 0.05f)); + // too far and driving away from cam + if(fwd.Magnitude() > 19.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + // too close + if(fwd.Magnitude() < 1.6f) + return true; + return false; + } + return true; + case OBBE_3: + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 28.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_1STPERSON: + return CTimer::GetTimeInMilliseconds() > t+3000; + case OBBE_5: + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return true; + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 28.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_ONSTRING: + return CTimer::GetTimeInMilliseconds() > t+3000; + case OBBE_COPCAR: + return CTimer::GetTimeInMilliseconds() > t+2000 && !FindPlayerVehicle()->GetIsOnScreen(); + case OBBE_COPCAR_WHEEL: + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return true; + if(CWorld::GetIsLineOfSightClear(pTargetEntity->GetPosition(), Cams[ActiveCam].Source, true, false, false, false, false, false, false)){ + if(CTimer::GetTimeInMilliseconds() > t+1000) + return true; + SetNearClipScript(0.6f); + return false; + } + return true; + + // Ped modes + case OBBE_9: + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 20.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_10: + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 8.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_11: + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 25.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_12: + if(CWorld::GetIsLineOfSightClear(FindPlayerCoors(), m_vecFixedModeSource, true, false, false, false, false, false, false)){ + fwd = FindPlayerCoors() - m_vecFixedModeSource; + fwd.z = 0.0f; + + // too far and driving away from cam + if(fwd.Magnitude() > 8.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return true; + return false; + } + return true; + case OBBE_13: + return CTimer::GetTimeInMilliseconds() > t+5000; + default: + return false; + } +} + +bool +CCamera::TryToStartNewCamMode(int obbeMode) +{ + CVehicle *veh; + CVector target, camPos, playerSpeed, fwd; + float ground; + bool foundGround; + int i; + + if(obbeMode < 0) + return true; + switch(obbeMode){ + case OBBE_WHEEL: + veh = FindPlayerVehicle(); + if(veh == nil || veh->IsBoat() || veh->GetModelIndex() == MI_RHINO) + return false; + target = Multiply3x3(FindPlayerVehicle()->GetMatrix(), CVector(-1.4f, -2.3f, 0.3f)); + target += FindPlayerVehicle()->GetPosition(); + if(!CWorld::GetIsLineOfSightClear(veh->GetPosition(), target, true, false, false, false, false, false, false)) + return false; + TakeControl(veh, CCam::MODE_WHEELCAM, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_1: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 20.0f*playerSpeed; + camPos += 3.0f*CVector(playerSpeed.y, -playerSpeed.x, 0.0f); + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return false; + + ground = CWorld::FindGroundZFor3DCoord(camPos.x, camPos.y, camPos.z+5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 1.5f; + else{ + ground = CWorld::FindRoofZFor3DCoord(camPos.x, camPos.y, camPos.z-5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 1.5f; + } + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + fwd = FindPlayerCoors() - camPos; + fwd.z = 0.0f; + // too far and driving away from cam + if(fwd.Magnitude() > 20.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return false; + // too close + if(fwd.Magnitude() < 1.6f) + return true; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_2: + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return false; + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 16.0f*playerSpeed; + camPos += 2.5f*CVector(playerSpeed.y, -playerSpeed.x, 0.0f); + + ground = CWorld::FindGroundZFor3DCoord(camPos.x, camPos.y, camPos.z+5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 0.5f; + else{ + ground = CWorld::FindRoofZFor3DCoord(camPos.x, camPos.y, camPos.z-5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 0.5f; + } + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + fwd = FindPlayerCoors() - camPos; + fwd.z = 0.0f; + // too far and driving away from cam + if(fwd.Magnitude() > 19.0f && DotProduct(FindPlayerSpeed(), fwd) > 0.0f) + return false; + // too close + if(fwd.Magnitude() < 1.6f) + return true; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_3: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 30.0f*playerSpeed; + camPos += 8.0f*CVector(playerSpeed.y, -playerSpeed.x, 0.0f); + + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_1STPERSON: + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_5: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 30.0f*playerSpeed; + camPos += 6.0f*CVector(playerSpeed.y, -playerSpeed.x, 0.0f); + + ground = CWorld::FindGroundZFor3DCoord(camPos.x, camPos.y, camPos.z+5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 3.5f; + else{ + ground = CWorld::FindRoofZFor3DCoord(camPos.x, camPos.y, camPos.z-5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 3.5f; + } + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_ONSTRING: + TakeControl(FindPlayerEntity(), CCam::MODE_CAM_ON_A_STRING, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_COPCAR: + if(FindPlayerPed()->m_pWanted->m_nWantedLevel < 1) + return false; + if(FindPlayerVehicle() == nil) + return false; + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return false; + i = CPools::GetVehiclePool()->GetSize(); + while(--i >= 0){ + veh = CPools::GetVehiclePool()->GetSlot(i); + if(veh && veh->IsCar() && veh != FindPlayerVehicle() && veh->bIsLawEnforcer){ + float dx = veh->GetPosition().x - FindPlayerCoors().x; + float dy = veh->GetPosition().y - FindPlayerCoors().y; + float dist = (veh->GetPosition() - FindPlayerCoors()).Magnitude(); + if(dist < 30.0f){ + if(dx*FindPlayerVehicle()->GetForward().x + dy*FindPlayerVehicle()->GetForward().y < 0.0f && + veh->GetForward().x*FindPlayerVehicle()->GetForward().x + veh->GetForward().y*FindPlayerVehicle()->GetForward().y > 0.8f){ + TakeControl(veh, CCam::MODE_CAM_ON_A_STRING, JUMP_CUT, CAMCONTROL_OBBE); + return true; + } + } + } + } + return false; + case OBBE_COPCAR_WHEEL: + if(FindPlayerPed()->m_pWanted->m_nWantedLevel < 1) + return false; + if(FindPlayerVehicle() == nil) + return false; + if(FindPlayerVehicle() && FindPlayerVehicle()->IsBoat()) + return false; + i = CPools::GetVehiclePool()->GetSize(); + while(--i >= 0){ + veh = CPools::GetVehiclePool()->GetSlot(i); + if(veh && veh->IsCar() && veh != FindPlayerVehicle() && veh->bIsLawEnforcer){ + float dx = veh->GetPosition().x - FindPlayerCoors().x; + float dy = veh->GetPosition().y - FindPlayerCoors().y; + float dist = (veh->GetPosition() - FindPlayerCoors()).Magnitude(); + if(dist < 30.0f){ + if(dx*FindPlayerVehicle()->GetForward().x + dy*FindPlayerVehicle()->GetForward().y < 0.0f && + veh->GetForward().x*FindPlayerVehicle()->GetForward().x + veh->GetForward().y*FindPlayerVehicle()->GetForward().y > 0.8f){ + target = Multiply3x3(veh->GetMatrix(), CVector(-1.4f, -2.3f, 0.3f)); + target += veh->GetPosition(); + if(!CWorld::GetIsLineOfSightClear(veh->GetPosition(), target, true, false, false, false, false, false, false)) + return false; + TakeControl(veh, CCam::MODE_WHEELCAM, JUMP_CUT, CAMCONTROL_OBBE); + return true; + } + } + } + } + return false; + + case OBBE_9: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 15.0f*playerSpeed; + camPos += CVector(2.0f, 1.0f, 0.0f); + + ground = CWorld::FindGroundZFor3DCoord(camPos.x, camPos.y, camPos.z+5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 0.5f; + else{ + ground = CWorld::FindRoofZFor3DCoord(camPos.x, camPos.y, camPos.z-5.0f, &foundGround); + if(foundGround) + camPos.z = ground + 0.5f; + } + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_10: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 5.0f*playerSpeed; + camPos += CVector(2.0f, 1.0f, 0.5f); + + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_11: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 20.0f*playerSpeed; + camPos += CVector(2.0f, 1.0f, 20.0f); + + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_12: + camPos = FindPlayerCoors(); + playerSpeed = FindPlayerSpeed(); + playerSpeed.z = 0.0f; + playerSpeed.Normalise(); + camPos += 5.0f*playerSpeed; + camPos += CVector(2.0f, 1.0f, 10.5f); + + if(!CWorld::GetIsLineOfSightClear(FindPlayerCoors(), camPos, true, false, false, false, false, false, false)) + return false; + + SetCamPositionForFixedMode(camPos, CVector(0.0f, 0.0f, 0.0f)); + TakeControl(FindPlayerEntity(), CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_OBBE); + return true; + case OBBE_13: +#ifdef FIX_BUGS + TakeControl(FindPlayerEntity(), CCam::MODE_TOP_DOWN_PED, JUMP_CUT, CAMCONTROL_OBBE); +#else + TakeControl(FindPlayerEntity(), CCam::MODE_TOPDOWN, JUMP_CUT, CAMCONTROL_OBBE); +#endif + return true; + default: + return false; + } +} + +static int32 SequenceOfCams[16] = { + OBBE_WHEEL, OBBE_COPCAR, OBBE_3, OBBE_1, OBBE_3, OBBE_COPCAR_WHEEL, + OBBE_2, OBBE_3, OBBE_COPCAR_WHEEL, OBBE_COPCAR, OBBE_2, OBBE_3, + OBBE_5, OBBE_3, + OBBE_ONSTRING // actually unused... +}; + +void +CCamera::ProcessObbeCinemaCameraCar(void) +{ + static int OldMode = -1; + static int32 TimeForNext = 0; + int i = 0; + + if(!bDidWeProcessAnyCinemaCam){ + OldMode = -1; + CHud::SetHelpMessage(TheText.Get("CINCAM"), true); + } + + if(!bDidWeProcessAnyCinemaCam || IsItTimeForNewcam(SequenceOfCams[OldMode], TimeForNext)){ + // This is very strange code... + for(OldMode = (OldMode+1) % 14; + !TryToStartNewCamMode(SequenceOfCams[OldMode]) && i <= 14; + OldMode = (OldMode+1) % 14) + i++; + TimeForNext = CTimer::GetTimeInMilliseconds(); + if(i >= 14){ + OldMode = 14; + TryToStartNewCamMode(SequenceOfCams[14]); + } + } + + m_iModeObbeCamIsInForCar = OldMode; + bDidWeProcessAnyCinemaCam = true; +} + +static int32 SequenceOfPedCams[5] = { OBBE_9, OBBE_10, OBBE_11, OBBE_12, OBBE_13 }; + +void +CCamera::ProcessObbeCinemaCameraPed(void) +{ + // static bool bObbePedProcessed = false; // unused + static int PedOldMode = -1; + static int32 PedTimeForNext = 0; + + if(!bDidWeProcessAnyCinemaCam) + PedOldMode = -1; + + if(!bDidWeProcessAnyCinemaCam || IsItTimeForNewcam(SequenceOfPedCams[PedOldMode], PedTimeForNext)){ + for(PedOldMode = (PedOldMode+1) % 5; + !TryToStartNewCamMode(SequenceOfPedCams[PedOldMode]); + PedOldMode = (PedOldMode+1) % 5); + PedTimeForNext = CTimer::GetTimeInMilliseconds(); + } + bDidWeProcessAnyCinemaCam = true; +} + +void +CCamera::DontProcessObbeCinemaCamera(void) +{ + bDidWeProcessAnyCinemaCam = false; +} + + +void +CCamera::LoadTrainCamNodes(char const *name) +{ + CFileMgr::SetDir("data"); + + char token[16] = { 0 }; + char filename[16] = { 0 }; + uint8 *buf; + int bufpos = 0; + int field = 0; + int tokpos = 0; + char c; + int i; + int len; + + strcpy(filename, name); + len = strlen(filename); + filename[len] = '.'; + filename[len+1] = 'd'; + filename[len+2] = 'a'; + filename[len+3] = 't'; + + m_uiNumberOfTrainCamNodes = 0; + + buf = new uint8[20000]; + len = CFileMgr::LoadFile(filename, buf, 20000, "r"); + + for(i = 0; i < MAX_NUM_OF_NODES; i++){ + m_arrTrainCamNode[i].m_cvecPointToLookAt = CVector(0.0f, 0.0f, 0.0f); + m_arrTrainCamNode[i].m_cvecMinPointInRange = CVector(0.0f, 0.0f, 0.0f); + m_arrTrainCamNode[i].m_cvecMaxPointInRange = CVector(0.0f, 0.0f, 0.0f); + m_arrTrainCamNode[i].m_fDesiredFOV = 0.0f; + m_arrTrainCamNode[i].m_fNearClip = 0.0f; + } + + while(bufpos <= len){ + c = buf[bufpos]; + switch(c){ + case '-': + case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': +// case '10': case '11': case '12': case '13': // ahem... + token[tokpos++] = c; + bufpos++; + break; + + case ',': + case ';': // game has the code for this duplicated but we handle both under the same case + switch((field+14)%14){ + case 0: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecCamPosition.x = atof(token); + break; + case 1: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecCamPosition.y = atof(token); + break; + case 2: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecCamPosition.z = atof(token); + break; + case 3: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecPointToLookAt.x = atof(token); + break; + case 4: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecPointToLookAt.y = atof(token); + break; + case 5: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecPointToLookAt.z = atof(token); + break; + case 6: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMinPointInRange.x = atof(token); + break; + case 7: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMinPointInRange.y = atof(token); + break; + case 8: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMinPointInRange.z = atof(token); + break; + case 9: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMaxPointInRange.x = atof(token); + break; + case 10: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMaxPointInRange.y = atof(token); + break; + case 11: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_cvecMaxPointInRange.z = atof(token); + break; + case 12: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_fDesiredFOV = atof(token); + break; + case 13: + m_arrTrainCamNode[m_uiNumberOfTrainCamNodes].m_fNearClip = atof(token); + m_uiNumberOfTrainCamNodes++; + break; + } + field++; + bufpos++; + memset(token, 0, sizeof(token)); + tokpos = 0; + break; + + default: + bufpos++; + break; + } + } + + delete[] buf; + CFileMgr::SetDir(""); +} + +void +CCamera::Process_Train_Camera_Control(void) +{ + bool found = false; + CTrain *target = (CTrain*)pTargetEntity; + m_bUseSpecialFovTrain = true; + static bool OKtoGoBackToNodeCam = true; // only ever set to true + uint32 i; + + if(target->m_nTrackId == TRACK_ELTRAIN && !m_bAboveGroundTrainNodesLoaded){ + m_bAboveGroundTrainNodesLoaded = true; + m_bBelowGroundTrainNodesLoaded = false; + LoadTrainCamNodes("Train"); + m_uiTimeLastChange = CTimer::GetTimeInMilliseconds(); + OKtoGoBackToNodeCam = true; + m_iCurrentTrainCamNode = 0; + } + if(target->m_nTrackId == TRACK_SUBWAY && !m_bBelowGroundTrainNodesLoaded){ + m_bBelowGroundTrainNodesLoaded = true; + m_bAboveGroundTrainNodesLoaded = false; + LoadTrainCamNodes("Train2"); + m_uiTimeLastChange = CTimer::GetTimeInMilliseconds(); + OKtoGoBackToNodeCam = true; + m_iCurrentTrainCamNode = 0; + } + + m_bTargetJustBeenOnTrain = true; + uint32 node = m_iCurrentTrainCamNode; + for(i = 0; i < m_uiNumberOfTrainCamNodes && !found; i++){ + if(target->IsWithinArea(m_arrTrainCamNode[node].m_cvecMinPointInRange.x, + m_arrTrainCamNode[node].m_cvecMinPointInRange.y, + m_arrTrainCamNode[node].m_cvecMinPointInRange.z, + m_arrTrainCamNode[node].m_cvecMaxPointInRange.x, + m_arrTrainCamNode[node].m_cvecMaxPointInRange.y, + m_arrTrainCamNode[node].m_cvecMaxPointInRange.z)){ + m_iCurrentTrainCamNode = node; + found = true; + } + node++; + if(node >= m_uiNumberOfTrainCamNodes) + node = 0; + } + + if(found){ + SetWideScreenOn(); + if(DotProduct(((CTrain*)pTargetEntity)->GetMoveSpeed(), pTargetEntity->GetForward()) < 0.001f){ + TakeControl(FindPlayerPed(), CCam::MODE_FOLLOWPED, JUMP_CUT, CAMCONTROL_SCRIPT); + if(target->Doors[0].IsFullyOpen()) + SetWideScreenOff(); + }else{ + SetCamPositionForFixedMode(m_arrTrainCamNode[m_iCurrentTrainCamNode].m_cvecCamPosition, CVector(0.0f, 0.0f, 0.0f)); + if(m_arrTrainCamNode[m_iCurrentTrainCamNode].m_cvecPointToLookAt.x == 999.0f && + m_arrTrainCamNode[m_iCurrentTrainCamNode].m_cvecPointToLookAt.y == 999.0f && + m_arrTrainCamNode[m_iCurrentTrainCamNode].m_cvecPointToLookAt.z == 999.0f) + TakeControl(target, CCam::MODE_FIXED, JUMP_CUT, CAMCONTROL_SCRIPT); + else + TakeControlNoEntity(m_arrTrainCamNode[m_iCurrentTrainCamNode].m_cvecPointToLookAt, JUMP_CUT, CAMCONTROL_SCRIPT); + RwCameraSetNearClipPlane(Scene.camera, m_arrTrainCamNode[m_iCurrentTrainCamNode].m_fNearClip); + } + }else{ + if(DotProduct(((CTrain*)pTargetEntity)->GetMoveSpeed(), pTargetEntity->GetForward()) < 0.001f){ + TakeControl(FindPlayerPed(), CCam::MODE_FOLLOWPED, JUMP_CUT, CAMCONTROL_SCRIPT); + if(target->Doors[0].IsFullyOpen()) + SetWideScreenOff(); + } + } +} + + + +void +CCamera::LoadPathSplines(int file) +{ + bool reading = true; + char c, token[32] = { 0 }; + int i, j, n; + + n = 0; + + for(i = 0; i < MAX_NUM_OF_SPLINETYPES; i++) + for(j = 0; j < CCamPathSplines::MAXPATHLENGTH; j++) + m_arrPathArray[i].m_arr_PathData[j] = 0.0f; + + m_bStartingSpline = false; + + i = 0; + j = 0; + while(reading){ + CFileMgr::Read(file, &c, 1); + switch(c){ + case '\0': + reading = false; + break; + + case '+': case '-': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'e': case 'E': + token[n++] = c; + break; + + case ',': +#ifdef FIX_BUGS + if(i < MAX_NUM_OF_SPLINETYPES && j < CCamPathSplines::MAXPATHLENGTH) +#endif + m_arrPathArray[i].m_arr_PathData[j] = atof(token); + j++; + memset(token, 0, 32); + n = 0; + break; + + case ';': +#ifdef FIX_BUGS + if(i < MAX_NUM_OF_SPLINETYPES && j < CCamPathSplines::MAXPATHLENGTH) +#endif + m_arrPathArray[i].m_arr_PathData[j] = atof(token); + i++; + j = 0; + memset(token, 0, 32); + n = 0; + } + } +} + +void +CCamera::FinishCutscene(void) +{ + SetPercentAlongCutScene(100.0f); + m_fPositionAlongSpline = 1.0f; + m_bcutsceneFinished = true; +} + +uint32 +CCamera::GetCutSceneFinishTime(void) +{ + int cam = ActiveCam; + if (Cams[cam].Mode == CCam::MODE_FLYBY) + return Cams[cam].m_uiFinishTime; + cam = (cam + 1) % 2; + if (Cams[cam].Mode == CCam::MODE_FLYBY) + return Cams[cam].m_uiFinishTime; + + return 0; +} + +void +CCamera::SetCamCutSceneOffSet(const CVector &pos) +{ + m_vecCutSceneOffset = pos; +}; + +void +CCamera::SetPercentAlongCutScene(float percent) +{ + if(Cams[ActiveCam].Mode == CCam::MODE_FLYBY) + Cams[ActiveCam].m_fTimeElapsedFloat = percent/100.0f * Cams[ActiveCam].m_uiFinishTime; + else if(Cams[(ActiveCam+1)%2].Mode == CCam::MODE_FLYBY) + Cams[(ActiveCam+1)%2].m_fTimeElapsedFloat = percent/100.0f * Cams[(ActiveCam+1)%2].m_uiFinishTime; +} + +void +CCamera::SetParametersForScriptInterpolation(float stopMoving, float catchUp, int32 time) +{ + m_fScriptPercentageInterToStopMoving = stopMoving * 0.01f; + m_fScriptPercentageInterToCatchUp = catchUp * 0.01f; + m_fScriptTimeForInterPolation = time; + m_bScriptParametersSetForInterPol = true; +} + +void +CCamera::SetZoomValueFollowPedScript(int16 dist) +{ + switch (dist) { + case 0: m_fPedZoomValueScript = 0.25f; break; + case 1: m_fPedZoomValueScript = 1.5f; break; + case 2: m_fPedZoomValueScript = 2.9f; break; + default: m_fPedZoomValueScript = m_fPedZoomValueScript; break; + } + + m_bUseScriptZoomValuePed = true; +} + +void +CCamera::SetZoomValueCamStringScript(int16 dist) +{ + switch (dist) { + case 0: m_fCarZoomValueScript = 0.05f; break; + case 1: m_fCarZoomValueScript = 1.9f; break; + case 2: m_fCarZoomValueScript = 3.9f; break; + default: m_fCarZoomValueScript = m_fCarZoomValueScript; break; + } + + m_bUseScriptZoomValueCar = true; +} + +void +CCamera::SetNearClipScript(float clip) +{ + m_fNearClipScript = clip; + m_bUseNearClipScript = true; +} + + + +void +CCamera::ProcessFade(void) +{ + float fade = (CTimer::GetTimeInMilliseconds() - m_uiFadeTimeStarted)/1000.0f; + // Why even set CDraw::FadeValue if m_fFLOATingFade sets it anyway? + if(m_bFading){ + if(m_iFadingDirection == FADE_IN){ + if(m_fTimeToFadeOut != 0.0f){ + m_fFLOATingFade = 255.0f - 255.0f*fade/m_fTimeToFadeOut; + if(m_fFLOATingFade <= 0.0f){ + m_bFading = false; + CDraw::FadeValue = 0; + m_fFLOATingFade = 0.0f; + } + }else{ + m_bFading = false; + CDraw::FadeValue = 0; + m_fFLOATingFade = 0.0f; + } + }else if(m_iFadingDirection == FADE_OUT){ + if(m_fTimeToFadeOut != 0.0f){ + m_fFLOATingFade = 255.0f*fade/m_fTimeToFadeOut; + if(m_fFLOATingFade >= 255.0f){ + m_bFading = false; + CDraw::FadeValue = 255; + m_fFLOATingFade = 255.0f; + } + }else{ + m_bFading = false; + CDraw::FadeValue = 255; + m_fFLOATingFade = 255.0f; + } + } + CDraw::FadeValue = m_fFLOATingFade; + } +} + +void +CCamera::ProcessMusicFade(void) +{ + float fade = (CTimer::GetTimeInMilliseconds() - m_uiFadeTimeStartedMusic)/1000.0f; + if(m_bMusicFading){ + if(m_iMusicFadingDirection == FADE_IN){ + if(m_fTimeToFadeMusic == 0.0f) + m_fTimeToFadeMusic = 1.0f; + + m_fFLOATingFadeMusic = 255.0f*fade/m_fTimeToFadeMusic; + if(m_fFLOATingFadeMusic > 255.0f){ + m_bMusicFading = false; + m_fFLOATingFadeMusic = 0.0f; + DMAudio.SetEffectsFadeVol(127); + DMAudio.SetMusicFadeVol(127); + }else{ + DMAudio.SetEffectsFadeVol(m_fFLOATingFadeMusic/255.0f * 127); + DMAudio.SetMusicFadeVol(m_fFLOATingFadeMusic/255.0f * 127); + } + }else if(m_iMusicFadingDirection == FADE_OUT){ + if(m_fTimeToFadeMusic == 0.0f) + m_fTimeToFadeMusic = 1.0f; + + if(m_bMoveCamToAvoidGeom || StillToFadeOut){ + m_fFLOATingFadeMusic = 256.0f; + m_bMoveCamToAvoidGeom = false; + }else + m_fFLOATingFadeMusic = 255.0f*fade/m_fTimeToFadeMusic; + + if(m_fFLOATingFadeMusic > 255.0f){ + m_bMusicFading = false; + m_fFLOATingFadeMusic = 255.0f; + DMAudio.SetEffectsFadeVol(0); + DMAudio.SetMusicFadeVol(0); + }else{ + DMAudio.SetEffectsFadeVol(127 - m_fFLOATingFadeMusic/255.0f * 127); + DMAudio.SetMusicFadeVol(127 - m_fFLOATingFadeMusic/255.0f * 127); + } + } + } +} + +void +CCamera::Fade(float timeout, int16 direction) +{ + m_bFading = true; + m_iFadingDirection = direction; + m_fTimeToFadeOut = timeout; + m_uiFadeTimeStarted = CTimer::GetTimeInMilliseconds(); + if(!m_bIgnoreFadingStuffForMusic){ + m_bMusicFading = true; + m_iMusicFadingDirection = direction; + m_fTimeToFadeMusic = timeout; + m_uiFadeTimeStartedMusic = CTimer::GetTimeInMilliseconds(); +// Not on PS2 + if(!m_bJustJumpedOutOf1stPersonBecauseOfTarget && m_iMusicFadingDirection == FADE_OUT){ + unknown++; + if(unknown >= 2){ + m_bJustJumpedOutOf1stPersonBecauseOfTarget = true; + unknown = 0; + }else + m_bMoveCamToAvoidGeom = true; + } + } +} + +void +CCamera::SetFadeColour(uint8 r, uint8 g, uint8 b) +{ + m_FadeTargetIsSplashScreen = r == 0 && g == 0 && b == 0; + CDraw::FadeRed = r; + CDraw::FadeGreen = g; + CDraw::FadeBlue = b; +} + +bool +CCamera::GetFading(void) { return m_bFading; } int -CCamera::GetFadingDirection() +CCamera::GetFadingDirection(void) { if(m_bFading) return m_iFadingDirection == FADE_IN ? FADE_IN : FADE_OUT; @@ -45,6 +3102,205 @@ CCamera::GetFadingDirection() return FADE_NONE; } +int +CCamera::GetScreenFadeStatus(void) +{ + if(m_fFLOATingFade == 0.0f) + return FADE_0; + if(m_fFLOATingFade == 255.0f) + return FADE_2; + return FADE_1; +} + + + +void +CCamera::RenderMotionBlur(void) +{ + if(m_BlurType == 0) + return; + + CMBlur::MotionBlurRender(m_pRwCamera, + m_BlurRed, m_BlurGreen, m_BlurBlue, + m_motionBlur, m_BlurType, m_imotionBlurAddAlpha); +} + +void +CCamera::SetMotionBlur(int r, int g, int b, int a, int type) +{ + m_BlurRed = r; + m_BlurGreen = g; + m_BlurBlue = b; + m_motionBlur = a; + m_BlurType = type; +} + +void +CCamera::SetMotionBlurAlpha(int a) +{ + m_imotionBlurAddAlpha = a; +} + + + +int +CCamera::GetLookDirection(void) +{ + if(Cams[ActiveCam].Mode == CCam::MODE_CAM_ON_A_STRING || + Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || + Cams[ActiveCam].Mode == CCam::MODE_BEHINDBOAT || + Cams[ActiveCam].Mode == CCam::MODE_FOLLOWPED) + return Cams[ActiveCam].DirectionWasLooking; + return LOOKING_FORWARD;; +} + +bool +CCamera::GetLookingForwardFirstPerson(void) +{ + return Cams[ActiveCam].Mode == CCam::MODE_1STPERSON && + Cams[ActiveCam].DirectionWasLooking == LOOKING_FORWARD; +} + +bool +CCamera::GetLookingLRBFirstPerson(void) +{ + return Cams[ActiveCam].Mode == CCam::MODE_1STPERSON && Cams[ActiveCam].DirectionWasLooking != LOOKING_FORWARD; +} + +void +CCamera::SetCameraDirectlyBehindForFollowPed_CamOnAString(void) +{ + m_bCamDirectlyBehind = true; + CPlayerPed *player = FindPlayerPed(); + if (player) + m_PedOrientForBehindOrInFront = CGeneral::GetATanOfXY(player->GetForward().x, player->GetForward().y); +} + +void +CCamera::SetCameraDirectlyInFrontForFollowPed_CamOnAString(void) +{ + m_bCamDirectlyInFront = true; + CPlayerPed *player = FindPlayerPed(); + if (player) + m_PedOrientForBehindOrInFront = CGeneral::GetATanOfXY(player->GetForward().x, player->GetForward().y); +} + +void +CCamera::SetNewPlayerWeaponMode(int16 mode, int16 minZoom, int16 maxZoom) +{ + PlayerWeaponMode.Mode = mode; + PlayerWeaponMode.MaxZoom = maxZoom; + PlayerWeaponMode.MinZoom = minZoom; + PlayerWeaponMode.Duration = 0.0f; +} + +void +CCamera::ClearPlayerWeaponMode(void) +{ + PlayerWeaponMode.Mode = 0; + PlayerWeaponMode.MaxZoom = 1; + PlayerWeaponMode.MinZoom = -1; + PlayerWeaponMode.Duration = 0.0f; +} + +void +CCamera::UpdateAimingCoors(CVector const &coors) +{ + m_cvecAimingTargetCoors = coors; +} + +void +CCamera::Find3rdPersonCamTargetVector(float dist, CVector pos, CVector &source, CVector &target) +{ + if(CPad::GetPad(0)->GetLookBehindForPed()){ + source = pos; + target = dist*Cams[ActiveCam].CamTargetEntity->GetForward() + source; + }else{ + float angleX = DEGTORAD((m_f3rdPersonCHairMultX-0.5f) * 1.8f * 0.5f * Cams[ActiveCam].FOV * CDraw::GetAspectRatio()); + float angleY = DEGTORAD((0.5f-m_f3rdPersonCHairMultY) * 1.8f * 0.5f * Cams[ActiveCam].FOV); + source = Cams[ActiveCam].Source; + target = Cams[ActiveCam].Front; + target += Cams[ActiveCam].Up * Tan(angleY); + target += CrossProduct(Cams[ActiveCam].Front, Cams[ActiveCam].Up) * Tan(angleX); + target.Normalise(); + float dot = DotProduct(pos - source, target); + source += dot*target; + target = dist*target + source; + } +} + +float +CCamera::Find3rdPersonQuickAimPitch(void) +{ + float clampedFrontZ = clamp(Cams[ActiveCam].Front.z, -1.0f, 1.0f); + + float rot = Asin(clampedFrontZ); + + return -(DEGTORAD(((0.5f - m_f3rdPersonCHairMultY) * 1.8f * 0.5f * Cams[ActiveCam].FOV)) + rot); +} + + + +void +CCamera::SetRwCamera(RwCamera *cam) +{ + m_pRwCamera = cam; + m_viewMatrix.Attach(&m_pRwCamera->viewMatrix, false); + CMBlur::MotionBlurOpen(m_pRwCamera); +} + +void +CCamera::CalculateDerivedValues(void) +{ + m_cameraMatrix = Invert(m_matrix); + + float hfov = DEGTORAD(CDraw::GetFOV()/2.0f); + float c = cos(hfov); + float s = sin(hfov); + + // right plane + m_vecFrustumNormals[0] = CVector(c, -s, 0.0f); + // left plane + m_vecFrustumNormals[1] = CVector(-c, -s, 0.0f); + + c /= CDraw::FindAspectRatio(); + s /= CDraw::FindAspectRatio(); + // bottom plane + m_vecFrustumNormals[2] = CVector(0.0f, -s, -c); + // top plane + m_vecFrustumNormals[3] = CVector(0.0f, -s, c); + + if(GetForward().x == 0.0f && GetForward().y == 0.0f) + GetForward().x = 0.0001f; + else + Orientation = Atan2(GetForward().x, GetForward().y); + + CamFrontXNorm = GetForward().x; + CamFrontYNorm = GetForward().y; + float l = Sqrt(SQR(CamFrontXNorm) + SQR(CamFrontYNorm)); + if(l == 0.0f) + CamFrontXNorm = 1.0f; + else{ + CamFrontXNorm /= l; + CamFrontYNorm /= l; + } +} + +bool +CCamera::IsPointVisible(const CVector ¢er, const CMatrix *mat) +{ + RwV3d c; + c = *(RwV3d*)¢er; + RwV3dTransformPoints(&c, &c, 1, &mat->m_matrix); + if(c.y < CDraw::GetNearClipZ()) return false; + if(c.y > CDraw::GetFarClipZ()) return false; + if(c.x*m_vecFrustumNormals[0].x + c.y*m_vecFrustumNormals[0].y > 0.0f) return false; + if(c.x*m_vecFrustumNormals[1].x + c.y*m_vecFrustumNormals[1].y > 0.0f) return false; + if(c.y*m_vecFrustumNormals[2].y + c.z*m_vecFrustumNormals[2].z > 0.0f) return false; + if(c.y*m_vecFrustumNormals[3].y + c.z*m_vecFrustumNormals[3].z > 0.0f) return false; + return true; +} + bool CCamera::IsSphereVisible(const CVector ¢er, float radius, const CMatrix *mat) { @@ -67,21 +3323,6 @@ CCamera::IsSphereVisible(const CVector ¢er, float radius) return IsSphereVisible(center, radius, &mat); } -bool -CCamera::IsPointVisible(const CVector ¢er, const CMatrix *mat) -{ - RwV3d c; - c = *(RwV3d*)¢er; - RwV3dTransformPoints(&c, &c, 1, &mat->m_matrix); - if(c.y < CDraw::GetNearClipZ()) return false; - if(c.y > CDraw::GetFarClipZ()) return false; - if(c.x*m_vecFrustumNormals[0].x + c.y*m_vecFrustumNormals[0].y > 0.0f) return false; - if(c.x*m_vecFrustumNormals[1].x + c.y*m_vecFrustumNormals[1].y > 0.0f) return false; - if(c.y*m_vecFrustumNormals[2].y + c.z*m_vecFrustumNormals[2].z > 0.0f) return false; - if(c.y*m_vecFrustumNormals[3].y + c.z*m_vecFrustumNormals[3].z > 0.0f) return false; - return true; -} - bool CCamera::IsBoxVisible(RwV3d *box, const CMatrix *mat) { @@ -104,225 +3345,15 @@ CCamera::IsBoxVisible(RwV3d *box, const CMatrix *mat) return true; } -int -CCamera::GetLookDirection(void) + + +CCamPathSplines::CCamPathSplines(void) { - if(Cams[ActiveCam].Mode == CCam::MODE_CAM_ON_A_STRING || - Cams[ActiveCam].Mode == CCam::MODE_1STPERSON || - Cams[ActiveCam].Mode == CCam::MODE_BEHINDBOAT || - Cams[ActiveCam].Mode == CCam::MODE_FOLLOWPED) - return Cams[ActiveCam].DirectionWasLooking; - return LOOKING_FORWARD;; + int i; + for(i = 0; i < MAXPATHLENGTH; i++) + m_arr_PathData[i] = 0.0f; } -bool -CCamera::GetLookingForwardFirstPerson() -{ - return Cams[ActiveCam].Mode == CCam::MODE_1STPERSON && - Cams[ActiveCam].DirectionWasLooking == LOOKING_FORWARD; -} - - -WRAPPER void CCamera::Fade(float timeout, int16 direction) { EAXJMP(0x46B3A0); } -WRAPPER void CCamera::ProcessFade(void) { EAXJMP(0x46F080); } -WRAPPER void CCamera::ProcessMusicFade(void) { EAXJMP(0x46F1E0); } - -int -CCamera::GetScreenFadeStatus(void) -{ - if(m_fFLOATingFade == 0.0f) - return FADE_0; - if(m_fFLOATingFade == 255.0f) - return FADE_2; - return FADE_1; -} - -void -CCamera::SetFadeColour(uint8 r, uint8 g, uint8 b) -{ - m_FadeTargetIsSplashScreen = r == 0 && g == 0 && b == 0; - CDraw::FadeRed = r; - CDraw::FadeGreen = g; - CDraw::FadeBlue = b; -} - -void -CCamera::SetMotionBlur(int r, int g, int b, int a, int type) -{ - m_BlurRed = r; - m_BlurGreen = g; - m_BlurBlue = b; - m_motionBlur = a; - m_BlurType = type; -} - -void -CCamera::SetMotionBlurAlpha(int a) -{ - m_imotionBlurAddAlpha = a; -} - -void -CCamera::SetNearClipScript(float clip) -{ - m_fNearClipScript = clip; - m_bUseNearClipScript = true; -} - -void -CCamera::RenderMotionBlur(void) -{ - if(m_BlurType == 0) - return; - - CMBlur::MotionBlurRender(m_pRwCamera, - m_BlurRed, m_BlurGreen, m_BlurBlue, - m_motionBlur, m_BlurType, m_imotionBlurAddAlpha); -} - -void -CCamera::ClearPlayerWeaponMode() -{ - PlayerWeaponMode.Mode = 0; - PlayerWeaponMode.MaxZoom = 1; - PlayerWeaponMode.MinZoom = -1; - PlayerWeaponMode.Duration = 0.0f; -} - -float -CCamera::Find3rdPersonQuickAimPitch(void) -{ - float clampedFrontZ = clamp(Cams[ActiveCam].Front.z, -1.0f, 1.0f); - - // float rot = atan2(clampedFrontZ, sqrt(1.0f - sq(clampedFrontZ))); - float rot = Asin(clampedFrontZ); - - return -(DEGTORAD(((0.5f - m_f3rdPersonCHairMultY) * 1.8f * 0.5f * Cams[ActiveCam].FOV)) + rot); -} - -void -CCamera::SetCamCutSceneOffSet(const CVector &pos) -{ - m_vecCutSceneOffset = pos; -}; - -void -CCamera::TakeControlWithSpline(short nSwitch) -{ - m_iModeToGoTo = CCam::MODE_FLYBY; - m_bLookingAtPlayer = false; - m_bLookingAtVector = false; - m_bcutsceneFinished = false; - m_iTypeOfSwitch = nSwitch; - m_bStartInterScript = true; - - //FindPlayerPed(); // unused -}; - -void CCamera::SetCameraDirectlyInFrontForFollowPed_CamOnAString() -{ - m_bCamDirectlyInFront = true; - CPlayerPed *player = FindPlayerPed(); - if (player) - m_PedOrientForBehindOrInFront = CGeneral::GetATanOfXY(player->GetForward().x, player->GetForward().y); -} - -void CCamera::SetCameraDirectlyBehindForFollowPed_CamOnAString() -{ - m_bCamDirectlyBehind = true; - CPlayerPed *player = FindPlayerPed(); - if (player) - m_PedOrientForBehindOrInFront = CGeneral::GetATanOfXY(player->GetForward().x, player->GetForward().y); -} - -void -CCamera::SetWideScreenOn(void) -{ - m_WideScreenOn = true; -} - -void -CCamera::SetWideScreenOff(void) -{ - m_bWantsToSwitchWidescreenOff = m_WideScreenOn; -} - -void -CCamera::SetNewPlayerWeaponMode(int16 mode, int16 minZoom, int16 maxZoom) -{ - PlayerWeaponMode.Mode = mode; - PlayerWeaponMode.MaxZoom = maxZoom; - PlayerWeaponMode.MinZoom = minZoom; - PlayerWeaponMode.Duration = 0.0f; -} - -void -CCamera::UpdateAimingCoors(CVector const &coors) -{ - m_cvecAimingTargetCoors = coors; -} - -void -CCamera::SetCamPositionForFixedMode(const CVector &Source, const CVector &UpOffSet) -{ - m_vecFixedModeSource = Source; - m_vecFixedModeUpOffSet = UpOffSet; -} - -void -CCamera::SetRwCamera(RwCamera *cam) -{ - m_pRwCamera = cam; - m_viewMatrix.Attach(&m_pRwCamera->viewMatrix, false); - CMBlur::MotionBlurOpen(m_pRwCamera); -} - -uint32 -CCamera::GetCutSceneFinishTime(void) -{ - int cam = ActiveCam; - if (Cams[cam].Mode == CCam::MODE_FLYBY) - return Cams[cam].m_uiFinishTime; - cam = (cam + 1) % 2; - if (Cams[cam].Mode == CCam::MODE_FLYBY) - return Cams[cam].m_uiFinishTime; - - return 0; -} - -void -CCamera::FinishCutscene(void) -{ - SetPercentAlongCutScene(100.0f); - m_fPositionAlongSpline = 1.0f; - m_bcutsceneFinished = true; -} - -void -CCamera::SetZoomValueFollowPedScript(int16 mode) -{ - switch (mode) { - case 0: m_fPedZoomValueScript = 0.25f; break; - case 1: m_fPedZoomValueScript = 1.5f; break; - case 2: m_fPedZoomValueScript = 2.9f; break; - default: m_fPedZoomValueScript = m_fPedZoomValueScript; break; - } - - m_bUseScriptZoomValuePed = true; -} - -void -CCamera::SetZoomValueCamStringScript(int16 mode) -{ - switch (mode) { - case 0: m_fCarZoomValueScript = 0.05f; break; - case 1: m_fCarZoomValueScript = 1.9f; break; - case 2: m_fCarZoomValueScript = 3.9f; break; - default: m_fCarZoomValueScript = m_fCarZoomValueScript; break; - } - - m_bUseScriptZoomValueCar = true; -} STARTPATCHES InjectHook(0x42C760, (bool (CCamera::*)(const CVector ¢er, float radius, const CMatrix *mat))&CCamera::IsSphereVisible, PATCH_JUMP); @@ -343,4 +3374,37 @@ STARTPATCHES InjectHook(0x46B560, &CCamera::FinishCutscene, PATCH_JUMP); InjectHook(0x46FF30, &CCamera::SetZoomValueFollowPedScript, PATCH_JUMP); InjectHook(0x46FF90, &CCamera::SetZoomValueCamStringScript, PATCH_JUMP); + + + InjectHook(0x46F8E0, &CCamera::ProcessWideScreenOn, PATCH_JUMP); + InjectHook(0x46FDE0, &CCamera::SetParametersForScriptInterpolation, PATCH_JUMP); + InjectHook(0x46BA20, &CCamera::GetLookingLRBFirstPerson, PATCH_JUMP); + InjectHook(0x470D80, &CCamera::StartTransitionWhenNotFinishedInter, PATCH_JUMP); + InjectHook(0x46FFF0, &CCamera::StartTransition, PATCH_JUMP); + InjectHook(0x46BEB0, &CCamera::InitialiseCameraForDebugMode, PATCH_JUMP); + InjectHook(0x471500, &CCamera::TakeControl, PATCH_JUMP); + InjectHook(0x4715B0, &CCamera::TakeControlNoEntity, PATCH_JUMP); + InjectHook(0x46B3A0, &CCamera::Fade, PATCH_JUMP); + InjectHook(0x46FE20, &CCamera::SetPercentAlongCutScene, PATCH_JUMP); + InjectHook(0x46B100, &CamShakeNoPos, PATCH_JUMP); + InjectHook(0x46B200, &CCamera::CamShake, PATCH_JUMP); + InjectHook(0x46F520, &CCamera::ProcessObbeCinemaCameraPed, PATCH_JUMP); + InjectHook(0x46F3E0, &CCamera::ProcessObbeCinemaCameraCar, PATCH_JUMP); + InjectHook(0x470DA0, &CCamera::StoreValuesDuringInterPol, PATCH_JUMP); + InjectHook(0x46B430, &CCamera::DrawBordersForWideScreen, PATCH_JUMP); + InjectHook(0x46F990, &CCamera::Restore, PATCH_JUMP); + InjectHook(0x46FAE0, &CCamera::RestoreWithJumpCut, PATCH_JUMP); + InjectHook(0x46F080, &CCamera::ProcessFade, PATCH_JUMP); + InjectHook(0x46EEA0, &CCamera::CalculateDerivedValues, PATCH_JUMP); + InjectHook(0x46F1E0, &CCamera::ProcessMusicFade, PATCH_JUMP); + InjectHook(0x46D1D0, &CCamera::LoadPathSplines, PATCH_JUMP); + InjectHook(0x4712A0, &CCamera::UpdateTargetEntity, PATCH_JUMP); + InjectHook(0x46B580, &CCamera::Find3rdPersonCamTargetVector, PATCH_JUMP); + InjectHook(0x46BAD0, &CCamera::Init, PATCH_JUMP); + InjectHook(0x46C9E0, &CCamera::LoadTrainCamNodes, PATCH_JUMP); + InjectHook(0x46F600, &CCamera::Process_Train_Camera_Control, PATCH_JUMP); + InjectHook(0x470EA0, &CCamera::UpdateSoundDistances, PATCH_JUMP); + InjectHook(0x46BF10, &CCamera::IsItTimeForNewcam, PATCH_JUMP); + InjectHook(0x471650, &CCamera::TryToStartNewCamMode, PATCH_JUMP); +// InjectHook(0x46D3F0, &CCamera::Process, PATCH_JUMP); ENDPATCHES diff --git a/src/core/Camera.h b/src/core/Camera.h index f3e3e661..f21fe913 100644 --- a/src/core/Camera.h +++ b/src/core/Camera.h @@ -4,13 +4,28 @@ class CEntity; class CPed; class CAutomobile; +class CGarage; extern int16 &DebugCamMode; -#define NUMBER_OF_VECTORS_FOR_AVERAGE 2 - -struct CCam +enum { + NUMBER_OF_VECTORS_FOR_AVERAGE = 2, + MAX_NUM_OF_SPLINETYPES = 4, + MAX_NUM_OF_NODES = 800 // for trains +}; + +#define DEFAULT_NEAR (0.9f) +#define CAM_ZOOM_1STPRS (0.0f) +#define CAM_ZOOM_1 (1.0f) +#define CAM_ZOOM_2 (2.0f) +#define CAM_ZOOM_3 (3.0f) +#define CAM_ZOOM_TOPDOWN (4.0f) +#define CAM_ZOOM_CINEMATIC (5.0f) + +class CCam +{ +public: enum { MODE_NONE = 0, @@ -230,9 +245,12 @@ static_assert(sizeof(CCam) == 0x1A4, "CCam: wrong size"); static_assert(offsetof(CCam, Alpha) == 0xA8, "CCam: error"); static_assert(offsetof(CCam, Front) == 0x140, "CCam: error"); -struct CCamPathSplines +class CCamPathSplines { - float m_arr_PathData[800]; +public: + enum {MAXPATHLENGTH=800}; + float m_arr_PathData[MAXPATHLENGTH]; + CCamPathSplines(void); }; struct CTrainCamNode @@ -296,13 +314,14 @@ enum enum { - CAM_CONTROLLER_0, - CAM_CONTROLLER_1, - CAM_CONTROLLER_2 + CAMCONTROL_GAME, + CAMCONTROL_SCRIPT, + CAMCONTROL_OBBE }; -struct CCamera : public CPlaceable +class CCamera : public CPlaceable { +public: bool m_bAboveGroundTrainNodesLoaded; bool m_bBelowGroundTrainNodesLoaded; bool m_bCamDirectlyBehind; @@ -344,16 +363,12 @@ struct CCamera : public CPlaceable bool m_bHeadBob; bool m_bFailedCullZoneTestPreviously; -bool m_FadeTargetIsSplashScreen; + bool m_FadeTargetIsSplashScreen; bool WorldViewerBeingUsed; uint8 ActiveCam; uint32 m_uiCamShakeStart; uint32 m_uiFirstPersonCamLastInputTime; -// where are those? -//bool m_bVehicleSuspenHigh; -//bool m_bEnable1rstPersonCamCntrlsScript; -//bool m_bAllow1rstPersonWeaponsCamera; uint32 m_uiLongestTimeInMill; uint32 m_uiNumberOfTrainCamNodes; @@ -369,7 +384,7 @@ bool m_FadeTargetIsSplashScreen; int m_BlurRed; int m_BlurType; -uint32 unknown; +uint32 unknown; // some counter having to do with music int m_iWorkOutSpeedThisNumFrames; int m_iNumFramesSoFar; @@ -412,20 +427,20 @@ uint32 unknown; float m_fOldBetaDiff; float m_fPedZoomValue; - float m_fPedZoomValueScript; - float m_fPedZoomValueSmooth; - float m_fPositionAlongSpline; - float m_ScreenReductionPercentage; - float m_ScreenReductionSpeed; - float m_AlphaForPlayerAnim1rstPerson; - float Orientation; - float PedZoomIndicator; - float PlayerExhaustion; - float SoundDistUp, SoundDistLeft, SoundDistRight; - float SoundDistUpAsRead, SoundDistLeftAsRead, SoundDistRightAsRead; - float SoundDistUpAsReadOld, SoundDistLeftAsReadOld, SoundDistRightAsReadOld; - float m_fWideScreenReductionAmount; - float m_fStartingFOVForInterPol; + float m_fPedZoomValueScript; + float m_fPedZoomValueSmooth; + float m_fPositionAlongSpline; + float m_ScreenReductionPercentage; + float m_ScreenReductionSpeed; + float m_AlphaForPlayerAnim1rstPerson; + float Orientation; + float PedZoomIndicator; + float PlayerExhaustion; + float SoundDistUp, SoundDistLeft, SoundDistRight; + float SoundDistUpAsRead, SoundDistLeftAsRead, SoundDistRightAsRead; + float SoundDistUpAsReadOld, SoundDistLeftAsReadOld, SoundDistRightAsReadOld; + float m_fWideScreenReductionAmount; + float m_fStartingFOVForInterPol; // not static yet float m_fMouseAccelHorzntl;// acceleration multiplier for 1st person controls @@ -435,8 +450,8 @@ uint32 unknown; CCam Cams[3]; - void *pToGarageWeAreIn; - void *pToGarageWeAreInForHackAvoidFirstPerson; + CGarage *pToGarageWeAreIn; + CGarage *pToGarageWeAreInForHackAvoidFirstPerson; CQueuedMode m_PlayerMode; CQueuedMode PlayerWeaponMode; CVector m_PreviousCameraPosition; @@ -447,17 +462,15 @@ uint32 unknown; CVector m_vecFixedModeUpOffSet; CVector m_vecCutSceneOffset; - // one of those has to go - CVector m_cvecStartingSourceForInterPol; - CVector m_cvecStartingTargetForInterPol; - CVector m_cvecStartingUpForInterPol; - CVector m_cvecSourceSpeedAtStartInter; - CVector m_cvecTargetSpeedAtStartInter; - CVector m_cvecUpSpeedAtStartInter; - CVector m_vecSourceWhenInterPol; - CVector m_vecTargetWhenInterPol; - CVector m_vecUpWhenInterPol; - //CVector m_vecClearGeometryVec; + CVector m_cvecStartingSourceForInterPol; + CVector m_cvecStartingTargetForInterPol; + CVector m_cvecStartingUpForInterPol; + CVector m_cvecSourceSpeedAtStartInter; + CVector m_cvecTargetSpeedAtStartInter; + CVector m_cvecUpSpeedAtStartInter; + CVector m_vecSourceWhenInterPol; + CVector m_vecTargetWhenInterPol; + CVector m_vecUpWhenInterPol; CVector m_vecGameCamPos; CVector SourceDuringInter; @@ -465,8 +478,8 @@ uint32 unknown; CVector UpDuringInter; RwCamera *m_pRwCamera; CEntity *pTargetEntity; - CCamPathSplines m_arrPathArray[4]; - CTrainCamNode m_arrTrainCamNode[800]; + CCamPathSplines m_arrPathArray[MAX_NUM_OF_SPLINETYPES]; + CTrainCamNode m_arrTrainCamNode[MAX_NUM_OF_NODES]; CMatrix m_cameraMatrix; bool m_bGarageFixedCamPositionSet; bool m_vecDoingSpecialInterPolation; @@ -490,7 +503,7 @@ uint32 unknown; float m_fScriptPercentageInterToStopMoving; float m_fScriptPercentageInterToCatchUp; -uint32 m_fScriptTimeForInterPolation; + uint32 m_fScriptTimeForInterPolation; int16 m_iFadingDirection; @@ -503,68 +516,97 @@ uint32 m_fScriptTimeForInterPolation; uint32 m_uiFadeTimeStartedMusic; static bool &m_bUseMouse3rdPerson; +#ifdef FREE_CAM + static bool bFreeCam; +#endif + // High level and misc + void Init(void); + void Process(void); + void CamControl(void); + void UpdateTargetEntity(void); + void UpdateSoundDistances(void); + void InitialiseCameraForDebugMode(void); + void CamShake(float strength, float x, float y, float z); bool Get_Just_Switched_Status() { return m_bJust_Switched; } - inline const CMatrix& GetCameraMatrix(void) { return m_cameraMatrix; } - CVector &GetGameCamPosition(void) { return m_vecGameCamPos; } + + // Who's in control + void TakeControl(CEntity *target, int16 mode, int16 typeOfSwitch, int32 controller); + void TakeControlNoEntity(const CVector &position, int16 typeOfSwitch, int32 controller); + void TakeControlWithSpline(int16 typeOfSwitch); + void Restore(void); + void RestoreWithJumpCut(void); + void SetCamPositionForFixedMode(const CVector &Source, const CVector &UppOffSet); + + // Transition + void StartTransition(int16 mode); + void StartTransitionWhenNotFinishedInter(int16 mode); + void StoreValuesDuringInterPol(CVector &source, CVector &target, CVector &up, float &FOV); + + // Widescreen borders + void SetWideScreenOn(void); + void SetWideScreenOff(void); + void ProcessWideScreenOn(void); + void DrawBordersForWideScreen(void); + + // Obbe's cam + bool IsItTimeForNewcam(int32 obbeMode, int32 time); + bool TryToStartNewCamMode(int32 obbeMode); + void DontProcessObbeCinemaCamera(void); + void ProcessObbeCinemaCameraCar(void); + void ProcessObbeCinemaCameraPed(void); + + // Train + void LoadTrainCamNodes(char const *name); + void Process_Train_Camera_Control(void); + + // Script + void LoadPathSplines(int file); + void FinishCutscene(void); float GetPositionAlongSpline(void) { return m_fPositionAlongSpline; } + uint32 GetCutSceneFinishTime(void); + void SetCamCutSceneOffSet(const CVector &pos); + void SetPercentAlongCutScene(float percent); + void SetParametersForScriptInterpolation(float stopMoving, float catchUp, int32 time); + void SetZoomValueFollowPedScript(int16 dist); + void SetZoomValueCamStringScript(int16 dist); + void SetNearClipScript(float); + + // Fading + void ProcessFade(void); + void ProcessMusicFade(void); + void Fade(float timeout, int16 direction); + void SetFadeColour(uint8 r, uint8 g, uint8 b); + bool GetFading(void); + int GetFadingDirection(void); + int GetScreenFadeStatus(void); + + // Motion blur + void RenderMotionBlur(void); + void SetMotionBlur(int r, int g, int b, int a, int type); + void SetMotionBlurAlpha(int a); + + // Player looking and aiming + int GetLookDirection(void); + bool GetLookingForwardFirstPerson(void); + bool GetLookingLRBFirstPerson(void); + void SetCameraDirectlyInFrontForFollowPed_CamOnAString(void); + void SetCameraDirectlyBehindForFollowPed_CamOnAString(void); + void SetNewPlayerWeaponMode(int16 mode, int16 minZoom, int16 maxZoom); + void ClearPlayerWeaponMode(void); + void UpdateAimingCoors(CVector const &coors); + void Find3rdPersonCamTargetVector(float dist, CVector pos, CVector &source, CVector &target); + float Find3rdPersonQuickAimPitch(void); + + // Physical camera + void SetRwCamera(RwCamera *cam); + const CMatrix& GetCameraMatrix(void) { return m_cameraMatrix; } + CVector &GetGameCamPosition(void) { return m_vecGameCamPos; } + void CalculateDerivedValues(void); bool IsPointVisible(const CVector ¢er, const CMatrix *mat); bool IsSphereVisible(const CVector ¢er, float radius, const CMatrix *mat); bool IsSphereVisible(const CVector ¢er, float radius); bool IsBoxVisible(RwV3d *box, const CMatrix *mat); - int GetLookDirection(void); - bool GetLookingForwardFirstPerson(void); - - void Fade(float timeout, int16 direction); - int GetScreenFadeStatus(void); - void ProcessFade(void); - void ProcessMusicFade(void); - void SetFadeColour(uint8 r, uint8 g, uint8 b); - - void CamShake(float strength, float x, float y, float z); - - void SetMotionBlur(int r, int g, int b, int a, int type); - void SetMotionBlurAlpha(int a); - void RenderMotionBlur(void); - void ClearPlayerWeaponMode(); - void CalculateDerivedValues(void); - - void DrawBordersForWideScreen(void); - void Restore(void); - void SetWideScreenOn(void); - void SetWideScreenOff(void); - void SetNearClipScript(float); - - float Find3rdPersonQuickAimPitch(void); - - void TakeControl(CEntity*, int16, int16, int32); - void TakeControlNoEntity(const CVector&, int16, int32); - void SetCamPositionForFixedMode(const CVector&, const CVector&); - bool GetFading(); - int GetFadingDirection(); - - void Init(); - void SetRwCamera(RwCamera*); - void Process(); - - void LoadPathSplines(int file); - uint32 GetCutSceneFinishTime(void); - void FinishCutscene(void); - - void SetCamCutSceneOffSet(const CVector&); - void TakeControlWithSpline(short); - void RestoreWithJumpCut(void); - void SetCameraDirectlyInFrontForFollowPed_CamOnAString(void); - void SetCameraDirectlyBehindForFollowPed_CamOnAString(void); - void SetZoomValueFollowPedScript(int16); - void SetZoomValueCamStringScript(int16); - void SetNewPlayerWeaponMode(int16, int16, int16); - void UpdateAimingCoors(CVector const &); - - void SetPercentAlongCutScene(float); - void SetParametersForScriptInterpolation(float, float, int32); - - void dtor(void) { this->CCamera::~CCamera(); } }; static_assert(offsetof(CCamera, DistanceToWater) == 0xe4, "CCamera: error"); static_assert(offsetof(CCamera, m_WideScreenOn) == 0x70, "CCamera: error"); @@ -583,3 +625,5 @@ static_assert(sizeof(CCamera) == 0xE9D8, "CCamera: wrong size"); extern CCamera &TheCamera; void CamShakeNoPos(CCamera*, float); +void MakeAngleLessThan180(float &Angle); +void WellBufferMe(float Target, float *CurrentValue, float *CurrentSpeed, float MaxSpeed, float Acceleration, bool IsAngle); diff --git a/src/core/CutsceneMgr.h b/src/core/CutsceneMgr.h index 381c71c9..7b809964 100644 --- a/src/core/CutsceneMgr.h +++ b/src/core/CutsceneMgr.h @@ -29,6 +29,7 @@ public: static void SetRunning(bool running) { ms_running = running; } static bool IsRunning(void) { return ms_running; } static bool IsCutsceneProcessing(void) { return ms_cutsceneProcessing; } + static bool UseLodMultiplier(void) { return ms_useLodMultiplier; } static CCutsceneObject* GetCutsceneObject(int id) { return ms_pCutsceneObjects[id]; } static int GetCutsceneTimeInMilleseconds(void) { return 1000.0f * ms_cutsceneTimer; } static char *GetCutsceneName(void) { return ms_cutsceneName; } diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 4fefe9a9..61fe96ea 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -529,7 +529,7 @@ WRAPPER void CMenuManager::DoSettingsBeforeStartingAGame() { EAXJMP(0x48AB40); } #else void CMenuManager::DoSettingsBeforeStartingAGame() { - CCamera::m_bUseMouse3rdPerson = m_ControlMethod == CONTROL_STANDART; + CCamera::m_bUseMouse3rdPerson = m_ControlMethod == CONTROL_STANDARD; if (m_PrefsVsyncDisp != m_PrefsVsync) m_PrefsVsync = m_PrefsVsyncDisp; @@ -2069,7 +2069,7 @@ void CMenuManager::Process(void) } if (m_nCurrScreen == MENUPAGE_LOADING_IN_PROGRESS) { if (CheckSlotDataValid(m_nCurrSaveSlot)) { - TheCamera.m_bUseMouse3rdPerson = m_ControlMethod == CONTROL_STANDART; + TheCamera.m_bUseMouse3rdPerson = m_ControlMethod == CONTROL_STANDARD; if (m_PrefsVsyncDisp != m_PrefsVsync) m_PrefsVsync = m_PrefsVsyncDisp; DMAudio.Service(); @@ -3166,7 +3166,7 @@ CMenuManager::ProcessButtonPresses(void) PSGLOBAL(joy1)->GetCapabilities(&devCaps); ControlsManager.InitDefaultControlConfigJoyPad(devCaps.dwButtons); } - CMenuManager::m_ControlMethod = CONTROL_STANDART; + CMenuManager::m_ControlMethod = CONTROL_STANDARD; MousePointerStateHelper.bInvertVertically = false; TheCamera.m_fMouseAccelHorzntl = 0.0025f; CVehicle::m_bDisableMouseSteering = true; @@ -3179,7 +3179,7 @@ CMenuManager::ProcessButtonPresses(void) #ifndef TIDY_UP_PBP if (CMenuManager::m_ControlMethod == CONTROL_CLASSIC) { CCamera::m_bUseMouse3rdPerson = true; - CMenuManager::m_ControlMethod = CONTROL_STANDART; + CMenuManager::m_ControlMethod = CONTROL_STANDARD; } else { CCamera::m_bUseMouse3rdPerson = false; CMenuManager::m_ControlMethod = CONTROL_CLASSIC; diff --git a/src/core/Frontend.h b/src/core/Frontend.h index 30e4f652..39e46ddd 100644 --- a/src/core/Frontend.h +++ b/src/core/Frontend.h @@ -363,7 +363,7 @@ enum enum eControlMethod { - CONTROL_STANDART = 0, + CONTROL_STANDARD = 0, CONTROL_CLASSIC, }; diff --git a/src/core/Pad.h b/src/core/Pad.h index 84919f32..ca44a9f7 100644 --- a/src/core/Pad.h +++ b/src/core/Pad.h @@ -2,7 +2,7 @@ enum { PLAYERCONTROL_ENABLED = 0, - PLAYERCONTROL_DISABLED_1 = 1, + PLAYERCONTROL_DISABLED_1 = 1, // used by first person camera PLAYERCONTROL_DISABLED_2 = 2, PLAYERCONTROL_GARAGE = 4, PLAYERCONTROL_DISABLED_8 = 8, diff --git a/src/core/config.h b/src/core/config.h index 58885e57..eb455de4 100644 --- a/src/core/config.h +++ b/src/core/config.h @@ -213,4 +213,4 @@ enum Config { // Camera #define IMPROVED_CAMERA // Better Debug cam, and maybe more in the future -//#define FREE_CAM // Rotating cam +#define FREE_CAM // Rotating cam diff --git a/src/core/re3.cpp b/src/core/re3.cpp index 05d28167..6d4ff252 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -372,11 +372,9 @@ DebugMenuPopulate(void) extern bool PrintDebugCode; extern int16 &DebugCamMode; + DebugMenuAddVarBool8("Cam", "Use mouse Cam", (int8*)&CCamera::m_bUseMouse3rdPerson, nil); #ifdef FREE_CAM - extern bool bFreePadCam; - extern bool bFreeMouseCam; - DebugMenuAddVarBool8("Cam", "Free Gamepad Cam", (int8*)&bFreePadCam, nil); - DebugMenuAddVarBool8("Cam", "Free Mouse Cam", (int8*)&bFreeMouseCam, nil); + DebugMenuAddVarBool8("Cam", "Free Cam", (int8*)&CCamera::bFreeCam, nil); #endif DebugMenuAddVarBool8("Cam", "Print Debug Code", (int8*)&PrintDebugCode, nil); DebugMenuAddVar("Cam", "Cam Mode", &DebugCamMode, nil, 1, 0, CCam::MODE_EDITOR, nil); diff --git a/src/peds/Ped.cpp b/src/peds/Ped.cpp index 264fa669..54816b1c 100644 --- a/src/peds/Ped.cpp +++ b/src/peds/Ped.cpp @@ -59,10 +59,6 @@ #define CAN_SEE_ENTITY_ANGLE_THRESHOLD DEGTORAD(60.0f) -#ifdef FREE_CAM -extern bool bFreeMouseCam; -#endif - CPed *gapTempPedList[50]; uint16 gnNumTempPedList; @@ -812,7 +808,7 @@ bool CPed::CanStrafeOrMouseControl(void) { #ifdef FREE_CAM - if (bFreeMouseCam) + if (CCamera::bFreeCam) return false; #endif return m_nPedState == PED_NONE || m_nPedState == PED_IDLE || m_nPedState == PED_FLEE_POS || m_nPedState == PED_FLEE_ENTITY || @@ -6993,7 +6989,7 @@ CPed::FinishLaunchCB(CAnimBlendAssociation *animAssoc, void *arg) ) { #ifdef FREE_CAM - if (TheCamera.Cams[0].Using3rdPersonMouseCam() && !bFreeMouseCam) { + if (TheCamera.Cams[0].Using3rdPersonMouseCam() && !CCamera::bFreeCam) { #else if (TheCamera.Cams[0].Using3rdPersonMouseCam()) { #endif diff --git a/src/peds/PlayerPed.cpp b/src/peds/PlayerPed.cpp index cd2cac23..6dbf7687 100644 --- a/src/peds/PlayerPed.cpp +++ b/src/peds/PlayerPed.cpp @@ -18,10 +18,6 @@ #define PAD_MOVE_TO_GAME_WORLD_MOVE 60.0f -#ifdef FREE_CAM -extern bool bFreeMouseCam; -#endif - CPlayerPed::~CPlayerPed() { delete m_pWanted; @@ -693,7 +689,7 @@ CPlayerPed::PlayerControl1stPersonRunAround(CPad *padUsed) float padMoveInGameUnit = padMove / PAD_MOVE_TO_GAME_WORLD_MOVE; if (padMoveInGameUnit > 0.0f) { #ifdef FREE_CAM - if (!bFreeMouseCam) + if (!CCamera::bFreeCam) m_fRotationDest = CGeneral::LimitRadianAngle(TheCamera.Orientation); else m_fRotationDest = CGeneral::GetRadianAngleBetweenPoints(0.0f, 0.0f, -leftRight, upDown) - TheCamera.Orientation; @@ -993,7 +989,7 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) SetStoredState(); m_nPedState = PED_SNIPER_MODE; #ifdef FREE_CAM - if (bFreeMouseCam && TheCamera.Cams[0].Using3rdPersonMouseCam()) { + if (CCamera::bFreeCam && TheCamera.Cams[0].Using3rdPersonMouseCam()) { m_fRotationCur = CGeneral::LimitRadianAngle(-TheCamera.Orientation); SetHeading(m_fRotationCur); } @@ -1018,7 +1014,7 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) if (m_nSelectedWepSlot == m_currentWeapon) { if (m_pPointGunAt) { #ifdef FREE_CAM - if (bFreeMouseCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE && m_fMoveSpeed < 1.0f) + if (CCamera::bFreeCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE && m_fMoveSpeed < 1.0f) StartFightAttack(padUsed->GetWeapon()); else #endif @@ -1052,7 +1048,7 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) #ifdef FREE_CAM // Rotate player/arm when shooting. We don't have auto-rotation anymore - if (CCamera::m_bUseMouse3rdPerson && bFreeMouseCam && + if (CCamera::m_bUseMouse3rdPerson && CCamera::bFreeCam && m_nSelectedWepSlot == m_currentWeapon && m_nMoveState != PEDMOVE_SPRINT) { // Weapons except throwable and melee ones @@ -1103,7 +1099,7 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) // what?? if (!m_pPointGunAt #ifdef FREE_CAM - || (!bFreeMouseCam && CCamera::m_bUseMouse3rdPerson) + || (!CCamera::bFreeCam && CCamera::m_bUseMouse3rdPerson) #else || CCamera::m_bUseMouse3rdPerson #endif @@ -1125,7 +1121,7 @@ CPlayerPed::ProcessPlayerWeapon(CPad *padUsed) TheCamera.UpdateAimingCoors(m_pPointGunAt->GetPosition()); } #ifdef FREE_CAM - else if ((bFreeMouseCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE) || (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson)) { + else if ((CCamera::bFreeCam && weaponInfo->m_eWeaponFire == WEAPON_FIRE_MELEE) || (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson)) { #else else if (weaponInfo->m_bCanAim && !CCamera::m_bUseMouse3rdPerson) { #endif diff --git a/src/vehicles/Automobile.cpp b/src/vehicles/Automobile.cpp index aca96aa3..e6b936f6 100644 --- a/src/vehicles/Automobile.cpp +++ b/src/vehicles/Automobile.cpp @@ -2340,8 +2340,7 @@ CAutomobile::FireTruckControl(void) if(!CPad::GetPad(0)->GetWeapon()) return; #ifdef FREE_CAM - extern bool bFreeMouseCam; - if (!bFreeMouseCam) + if (!CCamera::bFreeCam) #endif { m_fCarGunLR += CPad::GetPad(0)->GetCarGunLeftRight() * 0.00025f * CTimer::GetTimeStep(); @@ -2416,8 +2415,7 @@ CAutomobile::TankControl(void) // Rotate turret float prevAngle = m_fCarGunLR; #ifdef FREE_CAM - extern bool bFreeMouseCam; - if(!bFreeMouseCam) + if(!CCamera::bFreeCam) #endif m_fCarGunLR -= CPad::GetPad(0)->GetCarGunLeftRight() * 0.00015f * CTimer::GetTimeStep(); From 30b8d7300beb4f41cdaba27701a80f523301a268 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev Date: Sun, 5 Apr 2020 12:44:58 +0300 Subject: [PATCH 69/70] shoreside garage fix --- src/control/Garages.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/control/Garages.cpp b/src/control/Garages.cpp index 68d58b10..7e9fc0fa 100644 --- a/src/control/Garages.cpp +++ b/src/control/Garages.cpp @@ -1464,8 +1464,9 @@ void CGarage::UpdateDoorsHeight() void CGarage::BuildRotatedDoorMatrix(CEntity * pDoor, float fPosition) { float fAngle = -fPosition * HALFPI; - CVector r(-Sin(fAngle) * pDoor->GetForward().x, Sin(fAngle) * pDoor->GetForward().y, Cos(fAngle) * pDoor->GetForward().z); - pDoor->GetRight() = CrossProduct(r, pDoor->GetForward()); + CVector up(-Sin(fAngle) * pDoor->GetForward().y, Sin(fAngle) * pDoor->GetForward().z, Cos(fAngle)); + pDoor->GetRight() = CrossProduct(up, pDoor->GetForward()); + pDoor->GetUp() = up; } void CGarage::UpdateCrusherAngle() From 27f95d905c7af21183bfa4fbf4bc64a0176a7464 Mon Sep 17 00:00:00 2001 From: aap Date: Sun, 5 Apr 2020 15:27:30 +0200 Subject: [PATCH 70/70] fixed look behind bug --- src/core/Cam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/Cam.cpp b/src/core/Cam.cpp index b9e8e94e..5b7a53e9 100644 --- a/src/core/Cam.cpp +++ b/src/core/Cam.cpp @@ -627,7 +627,7 @@ CCam::LookBehind(void) DeltaBeta = TargetOrientation - Beta; while(DeltaBeta >= PI) DeltaBeta -= 2*PI; while(DeltaBeta < -PI) DeltaBeta += 2*PI; - if(DirectionWasLooking == LOOKING_BEHIND) + if(DirectionWasLooking != LOOKING_BEHIND) LookBehindCamWasInFront = DeltaBeta <= -HALFPI || DeltaBeta >= HALFPI; if(LookBehindCamWasInFront) TargetOrientation += PI;