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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 GnFIhX1K3vp literal 0 HcmV?d00001 diff --git a/gamefiles/fonts_j.txd b/gamefiles/fonts_j.txd new file mode 100644 index 0000000000000000000000000000000000000000..437e13f96b2fc655b02c048518ee3594c3c89a9c GIT binary patch literal 1052072 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 z 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�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 z%0A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%0D^e}DuhX4)d^L`#lyvbeP|S_075}S0>T+K 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 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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≪8)zXl`u;sgb|66>)g(;wQRN{^ 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 J|CxA@*#I{8Av*v7 literal 0 HcmV?d00001 diff --git a/gamefiles/russian.gxt b/gamefiles/russian.gxt new file mode 100644 index 0000000000000000000000000000000000000000..41c4cec8ffedee31fb182f2ee00c3286a8575526 GIT binary patch literal 220394 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%0AF&0jTE%T5qu)+(Ji;&w)k;K%;Z8`W@1y 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 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;