From ec1d14971c9bb1d751c0256093483d621dd719b8 Mon Sep 17 00:00:00 2001
From: Nikolay Korolev <nickvnuk@gmail.com>
Date: Wed, 27 May 2020 00:25:12 +0300
Subject: [PATCH] mission replay

---
 src/control/GameLogic.cpp       |  26 +++++-
 src/control/Script.cpp          | 147 ++++++++++++++++++++++++++++++++
 src/control/Script.h            |  19 +++++
 src/core/Frontend.cpp           |  19 +++++
 src/core/Frontend.h             |   6 +-
 src/core/MenuScreens.h          |  10 ++-
 src/core/Pools.cpp              |  43 ++++++++++
 src/core/config.h               |   2 +
 src/save/GenericGameStorage.cpp |  45 ++++++++++
 src/save/GenericGameStorage.h   |   8 +-
 src/save/PCSave.cpp             |   5 +-
 11 files changed, 325 insertions(+), 5 deletions(-)

diff --git a/src/control/GameLogic.cpp b/src/control/GameLogic.cpp
index abb5c5f3..ae26dd05 100644
--- a/src/control/GameLogic.cpp
+++ b/src/control/GameLogic.cpp
@@ -83,12 +83,20 @@ CGameLogic::Update()
 		}
 		break;
 	case WBSTATE_WASTED:
+#ifdef MISSION_REPLAY
+		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > AddExtraDeathDelay() + 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= AddExtraDeathDelay() + 0x800)) {
+#else
 		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= 0x800)) {
+#endif
 			TheCamera.SetFadeColour(200, 200, 200);
 			TheCamera.Fade(2.0f, FADE_OUT);
 		}
 
+#ifdef MISSION_REPLAY
+		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= AddExtraDeathDelay() + 0x1000) {
+#else
 		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= 0x1000) {
+#endif
 			pPlayerInfo.m_WBState = WBSTATE_PLAYING;
 			if (pPlayerInfo.m_bGetOutOfHospitalFree) {
 				pPlayerInfo.m_bGetOutOfHospitalFree = false;
@@ -131,11 +139,19 @@ CGameLogic::Update()
 		}
 		break;
 	case WBSTATE_BUSTED:
+#ifdef MISSION_REPLAY
+		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > AddExtraDeathDelay() + 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= AddExtraDeathDelay() + 0x800)) {
+#else
 		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= 0x800)) {
+#endif
 			TheCamera.SetFadeColour(0, 0, 0);
 			TheCamera.Fade(2.0f, FADE_OUT);
 		}
+#ifdef MISSION_REPLAY
+		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= AddExtraDeathDelay() + 0x1000) {
+#else
 		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= 0x1000) {
+#endif
 			pPlayerInfo.m_WBState = WBSTATE_PLAYING;
 			int takeMoney;
 
@@ -203,11 +219,19 @@ CGameLogic::Update()
 		}
 		break;
 	case WBSTATE_FAILED_CRITICAL_MISSION:
-		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > 0x800 && CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= 0x800) {
+#ifdef MISSION_REPLAY
+		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > AddExtraDeathDelay() + 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= AddExtraDeathDelay() + 0x800)) {
+#else
+		if ((CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime > 0x800) && (CTimer::GetPreviousTimeInMilliseconds() - pPlayerInfo.m_nWBTime <= 0x800)) {
+#endif
 			TheCamera.SetFadeColour(0, 0, 0);
 			TheCamera.Fade(2.0f, FADE_OUT);
 		}
+#ifdef MISSION_REPLAY
+		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= AddExtraDeathDelay() + 0x1000) {
+#else
 		if (CTimer::GetTimeInMilliseconds() - pPlayerInfo.m_nWBTime >= 0x1000) {
+#endif
 			pPlayerInfo.m_WBState = WBSTATE_PLAYING;
 			if (pPlayerInfo.m_pPed->bInVehicle) {
 				CVehicle *pVehicle = pPlayerInfo.m_pPed->m_pMyVehicle;
diff --git a/src/control/Script.cpp b/src/control/Script.cpp
index 406b11b7..549f53ad 100644
--- a/src/control/Script.cpp
+++ b/src/control/Script.cpp
@@ -28,6 +28,9 @@
 #include "Gangs.h"
 #include "Garages.h"
 #include "General.h"
+#ifdef MISSION_REPLAY
+#include "GenericGameStorage.h"
+#endif
 #include "HandlingMgr.h"
 #include "Heli.h"
 #include "Hud.h"
@@ -128,6 +131,38 @@ uint16 CTheScripts::CommandsExecuted;
 uint16 CTheScripts::ScriptsUpdated;
 int32 ScriptParams[32];
 
+#ifdef MISSION_REPLAY
+
+static const char* nonMissionScripts[] = {
+	"copcar",
+	"ambulan",
+	"taxi",
+	"firetru",
+	"rampage",
+	"t4x4_1",
+	"t4x4_2",
+	"t4x4_3",
+	"rc1",
+	"rc2",
+	"rc3",
+	"rc4",
+	"hj",
+	"usj",
+	"mayhem"
+};
+
+int AllowMissionReplay;
+uint32 NextMissionDelay;
+uint32 MissionStartTime;
+uint32 WaitForMissionActivate;
+uint32 WaitForSave;
+float oldTargetX;
+float oldTargetY;
+int missionRetryScriptIndex;
+bool doingMissionRetry;
+
+#endif
+
 
 const uint32 CRunningScript::nSaveStructSize =
 #ifdef COMPATIBLE_SAVES
@@ -666,6 +701,41 @@ void CTheScripts::Process()
 		if (UseTextCommands == 1)
 			UseTextCommands = 0;
 	}
+
+#ifdef MISSION_REPLAY
+	static uint32 TimeToWaitTill;
+	switch (AllowMissionReplay) {
+	case 2:
+		AllowMissionReplay = 3;
+		TimeToWaitTill = CTimer::GetTimeInMilliseconds() + (AddExtraDeathDelay() > 1000 ? 4000 : 2500);
+		break;
+	case 3:
+		if (TimeToWaitTill < CTimer::GetTimeInMilliseconds())
+			AllowMissionReplay = 4;
+		break;
+	case 4:
+		AllowMissionReplay = 5;
+		RetryMission(0, 0);
+	case 6:
+		AllowMissionReplay = 7;
+		TimeToWaitTill = CTimer::GetTimeInMilliseconds() + 500;
+	case 7:
+		if (TimeToWaitTill < CTimer::GetTimeInMilliseconds()) {
+			AllowMissionReplay = 0;
+			return;
+		}
+		break;
+	}
+	if (WaitForMissionActivate) {
+		if (WaitForMissionActivate > CTimer::GetTimeInMilliseconds())
+			return;
+		WaitForMissionActivate = 0;
+		WaitForSave = CTimer::GetTimeInMilliseconds() + 3000;
+	}
+	if (WaitForSave && WaitForSave > CTimer::GetTimeInMilliseconds())
+		WaitForSave = 0;
+#endif
+
 	CRunningScript* script = pActiveScripts;
 	while (script != nil){
 		CRunningScript* next = script->GetNext();
@@ -1239,6 +1309,17 @@ int8 CRunningScript::ProcessCommands0To99(int32 command)
 			CTheScripts::bAlreadyRunningAMissionScript = false;
 		RemoveScriptFromList(&CTheScripts::pActiveScripts);
 		AddScriptToList(&CTheScripts::pIdleScripts);
+#ifdef MISSION_REPLAY
+		if (m_bMissionFlag) {
+			CPlayerInfo* pPlayerInfo = &CWorld::Players[CWorld::PlayerInFocus];
+			if (pPlayerInfo->m_pPed->GetPedState() != PED_DEAD && pPlayerInfo->m_WBState == WBSTATE_PLAYING && !m_bDeatharrestExecuted)
+				SaveGameForPause(1);
+			oldTargetX = oldTargetY = 0.0f;
+			if (AllowMissionReplay == 1)
+				AllowMissionReplay = 2;
+			// I am fairly sure they forgot to set return value here
+		}
+#endif
 		return 1;
 	case COMMAND_START_NEW_SCRIPT:
 	{
@@ -2149,7 +2230,14 @@ int8 CRunningScript::ProcessCommands100To199(int32 command)
 		CollectParameters(&m_nIp, 2);
 		CVehicle* car = CPools::GetVehiclePool()->GetAt(ScriptParams[0]);
 		assert(car);
+#if defined MISSION_REPLAY && defined SIMPLIER_MISSIONS
+		car->AutoPilot.m_nCruiseSpeed = *(float*)&ScriptParams[1];
+		if (missionRetryScriptIndex == 40 && car->GetModelIndex() == MI_CHEETAH) // Turismo
+			car->AutoPilot.m_nCruiseSpeed = 8 * car->AutoPilot.m_nCruiseSpeed / 10;
+		car->AutoPilot.m_nCruiseSpeed = Min(car->AutoPilot.m_nCruiseSpeed, 60.0f * car->pHandling->Transmission.fUnkMaxVelocity);
+#else
 		car->AutoPilot.m_nCruiseSpeed = Min(*(float*)&ScriptParams[1], 60.0f * car->pHandling->Transmission.fUnkMaxVelocity);
+#endif
 		return 0;
 	}
 	case COMMAND_SET_CAR_DRIVING_STYLE:
@@ -2219,6 +2307,10 @@ int8 CRunningScript::ProcessCommands100To199(int32 command)
 	case COMMAND_PRINT_BIG:
 	{
 		wchar* key = TheText.Get((char*)&CTheScripts::ScriptSpace[m_nIp]);
+#ifdef MISSION_REPLAY
+		if (strcmp((char*)&CTheScripts::ScriptSpace[m_nIp], "M_FAIL") == 0 && CanAllowMissionReplay())
+			AllowMissionReplay = 1;
+#endif
 		m_nIp += KEY_LENGTH_IN_SCRIPT;
 		CollectParameters(&m_nIp, 2);
 		CMessages::AddBigMessage(key, ScriptParams[0], ScriptParams[1] - 1);
@@ -8479,6 +8571,10 @@ int8 CRunningScript::ProcessCommands1000To1099(int32 command)
 	case COMMAND_MAKE_PLAYER_SAFE_FOR_CUTSCENE:
 	{
 		CollectParameters(&m_nIp, 1);
+#ifdef MISSION_REPLAY
+		AllowMissionReplay = 0;
+		SaveGameForPause(3);
+#endif
 		CPlayerInfo* pPlayerInfo = &CWorld::Players[ScriptParams[0]];
 		CPad::GetPad(ScriptParams[0])->DisablePlayerControls |= PLAYERCONTROL_DISABLED_80;
 		pPlayerInfo->MakePlayerSafe(true);
@@ -8685,6 +8781,11 @@ int8 CRunningScript::ProcessCommands1000To1099(int32 command)
 	case COMMAND_LOAD_AND_LAUNCH_MISSION_INTERNAL:
 	{
 		CollectParameters(&m_nIp, 1);
+#ifdef MISSION_REPLAY
+		missionRetryScriptIndex = ScriptParams[0];
+		if (missionRetryScriptIndex == 19)
+			CStats::LastMissionPassedName[0] = '\0';
+#endif
 		CTimer::Suspend();
 		int offset = CTheScripts::MultiScriptArray[ScriptParams[0]];
 		CFileMgr::ChangeDir("\\");
@@ -11074,6 +11175,12 @@ void CRunningScript::DoDeatharrestCheck()
 	CPlayerInfo* pPlayer = &CWorld::Players[CWorld::PlayerInFocus];
 	if (!pPlayer->IsRestartingAfterDeath() && !pPlayer->IsRestartingAfterArrest() && !CTheScripts::UpsideDownCars.AreAnyCarsUpsideDown())
 		return;
+#ifdef MISSION_REPLAY
+	if (AllowMissionReplay != 0)
+		return;
+	if (CanAllowMissionReplay())
+		AllowMissionReplay = 1;
+#endif
 	assert(m_nStackPointer > 0);
 	while (m_nStackPointer > 1)
 		--m_nStackPointer;
@@ -11755,3 +11862,43 @@ void CRunningScript::Load(uint8*& buf)
 	prev = p;
 #endif
 }
+
+#ifdef MISSION_REPLAY
+
+bool CRunningScript::CanAllowMissionReplay()
+{
+	if (AllowMissionReplay)
+		return false;
+	if (CStats::LastMissionPassedName[0] == '\0')
+		return false;
+	for (int i = 0; i < ARRAY_SIZE(nonMissionScripts); i++) {
+		if (strcmp(m_abScriptName, nonMissionScripts[i]) == 0)
+			return false;
+	}
+	return true;
+}
+
+uint32 AddExtraDeathDelay()
+{
+	if (missionRetryScriptIndex == 63)
+		return 7000;
+	if (missionRetryScriptIndex == 64)
+		return 4000;
+	return 1000;
+}
+
+void RetryMission(int type, int unk)
+{
+	if (type == 0) {
+		doingMissionRetry = true;
+		FrontEndMenuManager.m_nCurrScreen = MENUPAGE_MISSION_RETRY;
+		FrontEndMenuManager.RequestFrontEndStartUp();
+	}
+	else if (type == 2) {
+		doingMissionRetry = false;
+		AllowMissionReplay = 6;
+		CTheScripts::MissionCleanup.Process();
+	}
+}
+
+#endif
diff --git a/src/control/Script.h b/src/control/Script.h
index acab66cc..743fad99 100644
--- a/src/control/Script.h
+++ b/src/control/Script.h
@@ -372,6 +372,9 @@ private:
 	friend class CRunningScript;
 	friend class CHud;
 	friend void CMissionCleanup::Process();
+#ifdef FIX_BUGS
+	friend void RetryMission(int, int);
+#endif
 };
 
 
@@ -479,6 +482,10 @@ private:
 	void CharInAreaCheckCommand(int32, uint32*);
 	void CarInAreaCheckCommand(int32, uint32*);
 
+#ifdef MISSION_REPLAY
+	bool CanAllowMissionReplay();
+#endif
+
 	float LimitAngleOnCircle(float angle) { return angle < 0.0f ? angle + 360.0f : angle; }
 
 	bool ThisIsAValidRandomPed(uint32 pedtype) {
@@ -502,3 +509,15 @@ private:
 		}
 	}
 };
+
+#ifdef MISSION_REPLAY
+extern int AllowMissionReplay;
+extern uint32 WaitForMissionActivate;
+extern uint32 WaitForSave;
+extern uint32 MissionStartTime;
+extern int missionRetryScriptIndex;
+extern bool doingMissionRetry;
+
+uint32 AddExtraDeathDelay();
+void RetryMission(int, int);
+#endif
diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp
index d58d0576..b291886f 100644
--- a/src/core/Frontend.cpp
+++ b/src/core/Frontend.cpp
@@ -3497,6 +3497,13 @@ CMenuManager::Process(void)
 				SaveLoadFileError_SetUpErrorScreen();
 		}
 		if (m_nCurrScreen == MENUPAGE_LOADING_IN_PROGRESS) {
+#ifdef MISSION_REPLAY
+			if (doingMissionRetry) {
+				RetryMission(2, 0);
+				m_nCurrSaveSlot = SLOT_COUNT;
+				doingMissionRetry = false;
+			}
+#endif
 			if (CheckSlotDataValid(m_nCurrSaveSlot)) {
 				TheCamera.m_bUseMouse3rdPerson = m_ControlMethod == CONTROL_STANDARD;
 				if (m_PrefsVsyncDisp != m_PrefsVsync)
@@ -4661,6 +4668,18 @@ CMenuManager::ProcessButtonPresses(void)
 					DMAudio.PlayFrontEndTrack(m_PrefsRadioStation, 1);
 					OutputDebugString("STARTED PLAYING FRONTEND AUDIO TRACK");
 					break;
+#ifdef MISSION_REPLAY
+				case MENUACTION_REJECT_RETRY:
+					doingMissionRetry = false;
+					AllowMissionReplay = 0;
+					RequestFrontEndShutDown();
+					break;
+				case MENUACTION_UNK114:
+					doingMissionRetry = false;
+					RequestFrontEndShutDown();
+					RetryMission(2, 0);
+					return;
+#endif
 			}
 		}
 		ProcessOnOffMenuOptions();
diff --git a/src/core/Frontend.h b/src/core/Frontend.h
index 89517528..b65a60f1 100644
--- a/src/core/Frontend.h
+++ b/src/core/Frontend.h
@@ -245,7 +245,7 @@ enum eMenuScreen
 	MENUPAGE_SKIN_SELECT = 54,
 	MENUPAGE_KEYBOARD_CONTROLS = 55,
 	MENUPAGE_MOUSE_CONTROLS = 56,
-	MENUPAGE_57 = 57, // mission failed, wanna restart page in mobile
+	MENUPAGE_MISSION_RETRY = 57,
 	MENUPAGE_58 = 58,
 #ifdef MENU_MAP
 	MENUPAGE_MAP = 59,
@@ -366,6 +366,10 @@ enum eMenuAction
 	MENUACTION_UNK108,
 	MENUACTION_UNK109,
 	MENUACTION_UNK110,
+	MENUACTION_UNK111,
+	MENUACTION_UNK112,
+	MENUACTION_REJECT_RETRY,
+	MENUACTION_UNK114,
 #ifdef MORE_LANGUAGES
 	MENUACTION_LANG_PL,
 	MENUACTION_LANG_RUS,
diff --git a/src/core/MenuScreens.h b/src/core/MenuScreens.h
index 6c2ebdb8..339ae2ce 100644
--- a/src/core/MenuScreens.h
+++ b/src/core/MenuScreens.h
@@ -445,11 +445,19 @@ const CMenuScreen aScreens[] = {
 	   MENUACTION_MOUSESTEER,	"FET_MST",	SAVESLOT_NONE, MENUPAGE_MOUSE_CONTROLS,
 	   MENUACTION_CHANGEMENU,	"FEDS_TB",	SAVESLOT_NONE, MENUPAGE_NONE,
    },
+	// MENUPAGE_MISSION_RETRY = 57
+#ifdef MISSION_REPLAY
 
-   // MENUPAGE_57 = 57
+   { "M_FAIL", 1, MENUPAGE_DISABLED, MENUPAGE_DISABLED, 0, 0,
+	   MENUACTION_LABEL,	    "FESZ_RM",  SAVESLOT_NONE, MENUPAGE_NONE,
+	   MENUACTION_CHANGEMENU,   "FEM_YES",  SAVESLOT_NONE, MENUPAGE_LOADING_IN_PROGRESS,
+	   MENUACTION_REJECT_RETRY, "FEM_NO",   SAVESLOT_NONE, MENUPAGE_NONE
+   },
+#else
    { "", 0, MENUPAGE_NONE, MENUPAGE_NONE, 0, 0,
 	   // mission failed, wanna restart page in mobile
    },
+#endif
 
    // MENUPAGE_58 = 58
    { "", 0, MENUPAGE_NONE, MENUPAGE_NONE, 0, 0,
diff --git a/src/core/Pools.cpp b/src/core/Pools.cpp
index 17b9e08d..bd0814d0 100644
--- a/src/core/Pools.cpp
+++ b/src/core/Pools.cpp
@@ -4,6 +4,9 @@
 
 #include "Boat.h"
 #include "CarCtrl.h"
+#ifdef MISSION_REPLAY
+#include "GenericGameStorage.h"
+#endif
 #include "Population.h"
 #include "ProjectileInfo.h"
 #include "Streaming.h"
@@ -206,11 +209,24 @@ INITSAVEBUF
 			if (pVehicle->pPassengers[j])
 				bHasPassenger = true;
 		}
+#ifdef MISSION_REPLAY
+		bool bForceSaving = CWorld::Players[CWorld::PlayerInFocus].m_pPed->m_pMyVehicle == pVehicle && IsQuickSave;
+#ifdef FIX_BUGS
+		if ((!pVehicle->pDriver && !bHasPassenger) || bForceSaving) {
+#else
+		if (!pVehicle->pDriver && !bHasPassenger) {
+#endif
+			if (pVehicle->IsCar() && (pVehicle->VehicleCreatedBy == MISSION_VEHICLE || bForceSaving))
+				++nNumCars;
+			if (pVehicle->IsBoat() && (pVehicle->VehicleCreatedBy == MISSION_VEHICLE || bForceSaving))
+				++nNumBoats;
+#else
 		if (!pVehicle->pDriver && !bHasPassenger) {
 			if (pVehicle->IsCar() && pVehicle->VehicleCreatedBy == MISSION_VEHICLE)
 				++nNumCars;
 			if (pVehicle->IsBoat() && pVehicle->VehicleCreatedBy == MISSION_VEHICLE)
 				++nNumBoats;
+#endif
 		}
 	}
 	*size = nNumCars * (sizeof(uint32) + sizeof(int16) + sizeof(int32) + CAutomobile::nSaveStructSize) + sizeof(int) +
@@ -226,23 +242,42 @@ INITSAVEBUF
 			if (pVehicle->pPassengers[j])
 				bHasPassenger = true;
 		}
+#ifdef MISSION_REPLAY
+		bool bForceSaving = CWorld::Players[CWorld::PlayerInFocus].m_pPed->m_pMyVehicle == pVehicle && IsQuickSave;
+#endif
+#if defined FIX_BUGS && defined MISSION_REPLAY
+		if ((!pVehicle->pDriver && !bHasPassenger) || bForceSaving) {
+#else
 		if (!pVehicle->pDriver && !bHasPassenger) {
+#endif
 #ifdef COMPATIBLE_SAVES
+#ifdef MISSION_REPLAY
+			if ((pVehicle->IsCar() || pVehicle->IsBoat()) && (pVehicle->VehicleCreatedBy == MISSION_VEHICLE || bForceSaving)) {
+#else
 			if ((pVehicle->IsCar() || pVehicle->IsBoat()) && pVehicle->VehicleCreatedBy == MISSION_VEHICLE) {
+#endif
 				WriteSaveBuf<uint32>(buf, pVehicle->m_vehType);
 				WriteSaveBuf<int16>(buf, pVehicle->GetModelIndex());
 				WriteSaveBuf<int32>(buf, GetVehicleRef(pVehicle));
 				pVehicle->Save(buf);
 			}
+#else
+#ifdef MISSION_REPLAY
+			if (pVehicle->IsCar() && (pVehicle->VehicleCreatedBy == MISSION_VEHICLE || bForceSaving)) {
 #else
 			if (pVehicle->IsCar() && pVehicle->VehicleCreatedBy == MISSION_VEHICLE) {
+#endif
 				WriteSaveBuf(buf, (uint32)pVehicle->m_vehType);
 				WriteSaveBuf(buf, pVehicle->GetModelIndex());
 				WriteSaveBuf(buf, GetVehicleRef(pVehicle));
 				memcpy(buf, pVehicle, sizeof(CAutomobile));
 				SkipSaveBuf(buf, sizeof(CAutomobile));
 			}
+#ifdef MISSION_REPLAY
+			if (pVehicle->IsBoat() && (pVehicle->VehicleCreatedBy == MISSION_VEHICLE || bForceSaving)) {
+#else
 			if (pVehicle->IsBoat() && pVehicle->VehicleCreatedBy == MISSION_VEHICLE) {
+#endif
 				WriteSaveBuf(buf, (uint32)pVehicle->m_vehType);
 				WriteSaveBuf(buf, pVehicle->GetModelIndex());
 				WriteSaveBuf(buf, GetVehicleRef(pVehicle));
@@ -400,7 +435,11 @@ INITSAVEBUF
 		CPed* pPed = GetPedPool()->GetSlot(i);
 		if (!pPed)
 			continue;
+#ifdef MISSION_REPLAY
+		if ((!pPed->bInVehicle || (pPed == CWorld::Players[CWorld::PlayerInFocus].m_pPed && IsQuickSave)) && pPed->m_nPedType == PEDTYPE_PLAYER1)
+#else
 		if (!pPed->bInVehicle && pPed->m_nPedType == PEDTYPE_PLAYER1)
+#endif
 			nNumPeds++;
 	}
 	*size = sizeof(int) + nNumPeds * (sizeof(uint32) + sizeof(int16) + sizeof(int) + CPlayerPed::nSaveStructSize +
@@ -410,7 +449,11 @@ INITSAVEBUF
 		CPed* pPed = GetPedPool()->GetSlot(i);
 		if (!pPed)
 			continue;
+#ifdef MISSION_REPLAY
+		if ((!pPed->bInVehicle || (pPed == CWorld::Players[CWorld::PlayerInFocus].m_pPed && IsQuickSave)) && pPed->m_nPedType == PEDTYPE_PLAYER1) {
+#else
 		if (!pPed->bInVehicle && pPed->m_nPedType == PEDTYPE_PLAYER1) {
+#endif
 			CopyToBuf(buf, pPed->m_nPedType);
 			CopyToBuf(buf, pPed->m_modelIndex);
 			int32 ref = GetPedRef(pPed);
diff --git a/src/core/config.h b/src/core/config.h
index f43067d7..5ad12fba 100644
--- a/src/core/config.h
+++ b/src/core/config.h
@@ -227,6 +227,8 @@ enum Config {
 #define USE_DEBUG_SCRIPT_LOADER	// makes game load main_freeroam.scm by default
 #define USE_MEASUREMENTS_IN_METERS // makes game use meters instead of feet in script
 #define USE_PRECISE_MEASUREMENT_CONVERTION // makes game convert feet to meeters more precisely
+#define MISSION_REPLAY // mobile feature
+//#define SIMPLIER_MISSIONS // apply simplifications from mobile
 
 #define COMPATIBLE_SAVES // this allows changing structs while keeping saves compatible
 
diff --git a/src/save/GenericGameStorage.cpp b/src/save/GenericGameStorage.cpp
index 8aae4011..eff0f2ff 100644
--- a/src/save/GenericGameStorage.cpp
+++ b/src/save/GenericGameStorage.cpp
@@ -42,6 +42,11 @@
 
 const uint32 SIZE_OF_ONE_GAME_IN_BYTES = 201729;
 
+#ifdef MISSION_REPLAY
+int8 IsQuickSave;
+const int PAUSE_SAVE_SLOT = SLOT_COUNT;
+#endif
+
 char DefaultPCSaveFileName[260];
 char ValidSaveName[260];
 char LoadFileName[256];
@@ -134,7 +139,12 @@ GenericSave(int file)
 	WriteDataToBufferPointer(buf, saveName);
 	GetLocalTime(&saveTime);
 	WriteDataToBufferPointer(buf, saveTime);
+#ifdef MISSION_REPLAY
+	int32 data = IsQuickSave << 24 | SIZE_OF_ONE_GAME_IN_BYTES;
+	WriteDataToBufferPointer(buf, data);
+#else
 	WriteDataToBufferPointer(buf, SIZE_OF_ONE_GAME_IN_BYTES);
+#endif
 	WriteDataToBufferPointer(buf, CGame::currLevel);
 	WriteDataToBufferPointer(buf, TheCamera.GetPosition().x);
 	WriteDataToBufferPointer(buf, TheCamera.GetPosition().y);
@@ -240,6 +250,9 @@ GenericLoad()
 	uint8 *buf;
 	int32 file;
 	uint32 size;
+#ifdef MISSION_REPLAY
+	int8 qs;
+#endif
 
 	int32 saveSize;
 	CPad *currPad;
@@ -254,6 +267,9 @@ GenericLoad()
 	ReadDataFromFile(file, work_buff, size);
 	buf = (work_buff + 0x40);
 	ReadDataFromBufferPointer(buf, saveSize);
+#ifdef MISSION_REPLAY // a hack to keep compatibility but get new data from save
+	qs = saveSize >> 24;
+#endif
 	ReadDataFromBufferPointer(buf, CGame::currLevel);
 	ReadDataFromBufferPointer(buf, TheCamera.GetMatrix().GetPosition().x);
 	ReadDataFromBufferPointer(buf, TheCamera.GetMatrix().GetPosition().y);
@@ -296,6 +312,11 @@ GenericLoad()
 	ReadDataFromBufferPointer(buf, TheCamera.PedZoomIndicator);
 #endif
 	assert(buf - work_buff == SIZE_OF_SIMPLEVARS);
+#ifdef MISSION_REPLAY
+	WaitForSave = 0;
+	if (FrontEndMenuManager.m_nCurrSaveSlot == PAUSE_SAVE_SLOT && qs == 3)
+		WaitForMissionActivate = CTimer::GetTimeInMilliseconds() + 2000;
+#endif
 	ReadDataFromBlock("Loading Scripts \n", CTheScripts::LoadAllScripts);
 
 	// Load the rest
@@ -563,3 +584,27 @@ align4bytes(int32 size)
 {
 	return (size + 3) & 0xFFFFFFFC;
 }
+
+#ifdef MISSION_REPLAY
+
+void DisplaySaveResult(int unk, char* name)
+{}
+
+bool SaveGameForPause(int type)
+{
+	if (AllowMissionReplay != 0 || type != 3 && WaitForSave > CTimer::GetTimeInMilliseconds())
+		return false;
+	WaitForSave = 0;
+	if (gGameState != GS_PLAYING_GAME || CTheScripts::IsPlayerOnAMission() || CStats::LastMissionPassedName[0] == '\0') {
+		DisplaySaveResult(3, CStats::LastMissionPassedName);
+		return false;
+	}
+	IsQuickSave = type;
+	MissionStartTime = 0;
+	int res = PcSaveHelper.SaveSlot(PAUSE_SAVE_SLOT);
+	PcSaveHelper.PopulateSlotInfo();
+	IsQuickSave = 0;
+	DisplaySaveResult(res, CStats::LastMissionPassedName);
+	return true;
+}
+#endif
diff --git a/src/save/GenericGameStorage.h b/src/save/GenericGameStorage.h
index b913c305..ee8a52a1 100644
--- a/src/save/GenericGameStorage.h
+++ b/src/save/GenericGameStorage.h
@@ -40,4 +40,10 @@ extern uint32 TimeToStayFadedBeforeFadeOut;
 
 extern char SaveFileNameJustSaved[260]; // 8F2570
 
-const char TopLineEmptyFile[] = "THIS FILE IS NOT VALID YET";
\ No newline at end of file
+const char TopLineEmptyFile[] = "THIS FILE IS NOT VALID YET";
+
+#ifdef MISSION_REPLAY
+extern int8 IsQuickSave; // originally int
+
+bool SaveGameForPause(int);
+#endif
diff --git a/src/save/PCSave.cpp b/src/save/PCSave.cpp
index 3dc80f73..3103c7ab 100644
--- a/src/save/PCSave.cpp
+++ b/src/save/PCSave.cpp
@@ -41,7 +41,10 @@ C_PcSave::SaveSlot(int32 slot)
 	_psGetUserFilesFolder();
 	int file = CFileMgr::OpenFile(ValidSaveName, "wb");
 	if (file != 0) {
-		DoGameSpecificStuffBeforeSave();
+#ifdef MISSION_REPLAY
+		if (!IsQuickSave)
+#endif
+			DoGameSpecificStuffBeforeSave();
 		if (GenericSave(file)) {
 			if (!!CFileMgr::CloseFile(file))
 				nErrorCode = SAVESTATUS_ERR_SAVE_CLOSE;