mirror of
https://github.com/PCSX2/pcsx2.git
synced 2026-01-31 01:15:24 +01:00
471 lines
14 KiB
C++
471 lines
14 KiB
C++
/* PCSX2 - PS2 Emulator for PCs
|
|
* Copyright (C) 2002-2020 PCSX2 Dev Team
|
|
*
|
|
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
|
|
* of the GNU Lesser General Public License as published by the Free Software Found-
|
|
* ation, either version 3 of the License, or (at your option) any later version.
|
|
*
|
|
* PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
|
* PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with PCSX2.
|
|
* If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "PrecompiledHeader.h"
|
|
|
|
#include "common/StringUtil.h"
|
|
#include "SaveState.h"
|
|
#include "Counters.h"
|
|
#include "SaveState.h"
|
|
|
|
#ifndef DISABLE_RECORDING
|
|
|
|
#include "gui/App.h"
|
|
#include "gui/AppSaveStates.h"
|
|
#include "DebugTools/Debug.h"
|
|
#include "GameDatabase.h"
|
|
|
|
#include "InputRecording.h"
|
|
#include "InputRecordingControls.h"
|
|
#include "Utilities/InputRecordingLogger.h"
|
|
|
|
#include "gui/AppSaveStates.h"
|
|
|
|
#include <fmt/format.h>
|
|
|
|
#endif
|
|
|
|
void SaveStateBase::InputRecordingFreeze()
|
|
{
|
|
// NOTE - BE CAREFUL
|
|
// CHANGING THIS WILL BREAK BACKWARDS COMPATIBILITY ON SAVESTATES
|
|
FreezeTag("InputRecording");
|
|
Freeze(g_FrameCount);
|
|
|
|
#ifndef DISABLE_RECORDING
|
|
if (g_Conf->EmuOptions.EnableRecordingTools)
|
|
{
|
|
// Loading a save-state is an asynchronous task. If we are playing a recording
|
|
// that starts from a savestate (not power-on) and the starting (pcsx2 internal) frame
|
|
// marker has not been set (which comes from the save-state), we initialize it.
|
|
if (g_InputRecording.IsInitialLoad())
|
|
g_InputRecording.SetupInitialState(g_FrameCount);
|
|
else if (g_InputRecording.IsActive() && IsLoading())
|
|
g_InputRecording.SetFrameCounter(g_FrameCount);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifndef DISABLE_RECORDING
|
|
|
|
InputRecording g_InputRecording;
|
|
|
|
InputRecording::InputRecordingPad::InputRecordingPad()
|
|
{
|
|
padData = new PadData;
|
|
}
|
|
|
|
InputRecording::InputRecordingPad::~InputRecordingPad()
|
|
{
|
|
delete padData;
|
|
}
|
|
|
|
void InputRecording::InitVirtualPadWindows(wxWindow* parent)
|
|
{
|
|
for (int port = 0; port < 2; ++port)
|
|
if (!pads[port].virtualPad)
|
|
pads[port].virtualPad = new VirtualPad(parent, port, g_Conf->inputRecording);
|
|
}
|
|
|
|
void InputRecording::ShowVirtualPad(const int port)
|
|
{
|
|
pads[port].virtualPad->Show();
|
|
}
|
|
|
|
void InputRecording::RecordingReset()
|
|
{
|
|
// Booting is an asynchronous task. If we are playing a recording
|
|
// that starts from power-on and the starting (pcsx2 internal) frame
|
|
// marker has not been set, we initialize it.
|
|
if (g_InputRecording.IsInitialLoad())
|
|
g_InputRecording.SetupInitialState(0);
|
|
else if (g_InputRecording.IsActive())
|
|
{
|
|
g_InputRecording.SetFrameCounter(0);
|
|
g_InputRecordingControls.Lock(0);
|
|
}
|
|
else
|
|
g_InputRecordingControls.Resume();
|
|
}
|
|
|
|
void InputRecording::ControllerInterrupt(u8& data, u8& port, u16& bufCount, u8 buf[])
|
|
{
|
|
// TODO - Multi-Tap Support
|
|
|
|
if (bufCount == 1)
|
|
fInterruptFrame = data == READ_DATA_AND_VIBRATE_FIRST_BYTE;
|
|
else if (bufCount == 2)
|
|
{
|
|
if (buf[bufCount] != READ_DATA_AND_VIBRATE_SECOND_BYTE)
|
|
fInterruptFrame = false;
|
|
}
|
|
else if (fInterruptFrame)
|
|
{
|
|
u8& bufVal = buf[bufCount];
|
|
const u16 bufIndex = bufCount - 3;
|
|
if (state == InputRecordingMode::Replaying)
|
|
{
|
|
if (frameCounter >= 0 && frameCounter < INT_MAX)
|
|
{
|
|
if (!inputRecordingData.ReadKeyBuffer(bufVal, frameCounter, port, bufIndex))
|
|
inputRec::consoleLog(fmt::format("Failed to read input data at frame {}", frameCounter));
|
|
|
|
// Update controller data state for future VirtualPad / logging usage.
|
|
pads[port].padData->UpdateControllerData(bufIndex, bufVal);
|
|
|
|
if (pads[port].virtualPad->IsShown())
|
|
pads[port].virtualPad->UpdateControllerData(bufIndex, pads[port].padData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Update controller data state for future VirtualPad / logging usage.
|
|
pads[port].padData->UpdateControllerData(bufIndex, bufVal);
|
|
|
|
// Commit the byte to the movie file if we are recording
|
|
if (state == InputRecordingMode::Recording)
|
|
{
|
|
if (frameCounter >= 0)
|
|
{
|
|
// If the VirtualPad updated the PadData, we have to update the buffer
|
|
// before committing it to the recording / sending it to the game
|
|
if (pads[port].virtualPad->IsShown() && pads[port].virtualPad->UpdateControllerData(bufIndex, pads[port].padData))
|
|
bufVal = pads[port].padData->PollControllerData(bufIndex);
|
|
|
|
if (incrementUndo)
|
|
{
|
|
inputRecordingData.IncrementUndoCount();
|
|
incrementUndo = false;
|
|
}
|
|
|
|
if (frameCounter < INT_MAX && !inputRecordingData.WriteKeyBuffer(frameCounter, port, bufIndex, bufVal))
|
|
inputRec::consoleLog(fmt::format("Failed to write input data at frame {}", frameCounter));
|
|
}
|
|
}
|
|
// If the VirtualPad updated the PadData, we have to update the buffer
|
|
// before sending it to the game
|
|
else if (pads[port].virtualPad->IsShown() && pads[port].virtualPad->UpdateControllerData(bufIndex, pads[port].padData))
|
|
bufVal = pads[port].padData->PollControllerData(bufIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
s32 InputRecording::GetFrameCounter()
|
|
{
|
|
return frameCounter;
|
|
}
|
|
|
|
InputRecordingFile& InputRecording::GetInputRecordingData()
|
|
{
|
|
return inputRecordingData;
|
|
}
|
|
|
|
u32 InputRecording::GetStartingFrame()
|
|
{
|
|
return startingFrame;
|
|
}
|
|
|
|
void InputRecording::IncrementFrameCounter()
|
|
{
|
|
if (frameCounter < INT_MAX)
|
|
{
|
|
frameCounter++;
|
|
switch (state)
|
|
{
|
|
case InputRecordingMode::Recording:
|
|
inputRecordingData.SetTotalFrames(frameCounter);
|
|
[[fallthrough]];
|
|
case InputRecordingMode::Replaying:
|
|
if (frameCounter == inputRecordingData.GetTotalFrames())
|
|
incrementUndo = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void InputRecording::LogAndRedraw()
|
|
{
|
|
for (u8 port = 0; port < 2; port++)
|
|
{
|
|
pads[port].padData->LogPadData(port);
|
|
// As well as re-render the virtual pad UI, if applicable
|
|
// - Don't render if it's minimized
|
|
if (pads[port].virtualPad->IsShown() && !pads[port].virtualPad->IsIconized())
|
|
pads[port].virtualPad->Redraw();
|
|
}
|
|
}
|
|
|
|
bool InputRecording::IsInterruptFrame()
|
|
{
|
|
return fInterruptFrame;
|
|
}
|
|
|
|
bool InputRecording::IsActive()
|
|
{
|
|
return state != InputRecordingMode::NotActive;
|
|
}
|
|
|
|
bool InputRecording::IsInitialLoad()
|
|
{
|
|
return initialLoad;
|
|
}
|
|
|
|
bool InputRecording::IsReplaying()
|
|
{
|
|
return state == InputRecordingMode::Replaying;
|
|
}
|
|
|
|
bool InputRecording::IsRecording()
|
|
{
|
|
return state == InputRecordingMode::Recording;
|
|
}
|
|
|
|
wxString InputRecording::RecordingModeTitleSegment()
|
|
{
|
|
switch (state)
|
|
{
|
|
case InputRecordingMode::Recording:
|
|
return wxString("Recording");
|
|
case InputRecordingMode::Replaying:
|
|
return wxString("Replaying");
|
|
default:
|
|
return wxString("No Movie");
|
|
}
|
|
}
|
|
|
|
void InputRecording::SetToRecordMode()
|
|
{
|
|
state = InputRecordingMode::Recording;
|
|
pads[CONTROLLER_PORT_ONE].virtualPad->SetReadOnlyMode(false);
|
|
pads[CONTROLLER_PORT_TWO].virtualPad->SetReadOnlyMode(false);
|
|
inputRec::log("Record mode ON");
|
|
}
|
|
|
|
void InputRecording::SetToReplayMode()
|
|
{
|
|
state = InputRecordingMode::Replaying;
|
|
pads[CONTROLLER_PORT_ONE].virtualPad->SetReadOnlyMode(true);
|
|
pads[CONTROLLER_PORT_TWO].virtualPad->SetReadOnlyMode(true);
|
|
inputRec::log("Replay mode ON");
|
|
}
|
|
|
|
void InputRecording::SetFrameCounter(u32 newGFrameCount)
|
|
{
|
|
if (newGFrameCount > startingFrame + (u32)inputRecordingData.GetTotalFrames())
|
|
{
|
|
inputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point after the end of the original recording. This should be avoided.");
|
|
inputRec::consoleLog("Savestate's framecount has been ignored.");
|
|
frameCounter = inputRecordingData.GetTotalFrames();
|
|
if (state == InputRecordingMode::Replaying)
|
|
SetToRecordMode();
|
|
incrementUndo = false;
|
|
}
|
|
else
|
|
{
|
|
if (newGFrameCount < startingFrame)
|
|
{
|
|
inputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point before the start of the original recording. This should be avoided.");
|
|
if (state == InputRecordingMode::Recording)
|
|
SetToReplayMode();
|
|
}
|
|
else if (newGFrameCount == 0 && state == InputRecordingMode::Recording)
|
|
SetToReplayMode();
|
|
frameCounter = newGFrameCount - (s32)startingFrame;
|
|
incrementUndo = true;
|
|
}
|
|
}
|
|
|
|
void InputRecording::SetupInitialState(u32 newStartingFrame)
|
|
{
|
|
startingFrame = newStartingFrame;
|
|
if (state != InputRecordingMode::Replaying)
|
|
{
|
|
inputRec::log("Started new input recording");
|
|
inputRec::consoleLog(fmt::format("Filename {}", inputRecordingData.GetFilename().ToUTF8()));
|
|
SetToRecordMode();
|
|
}
|
|
else
|
|
{
|
|
// Check if the current game matches with the one used to make the original recording
|
|
if (!g_Conf->CurrentIso.IsEmpty())
|
|
if (resolveGameName() != inputRecordingData.GetHeader().gameName)
|
|
inputRec::consoleLog("Input recording was possibly constructed for a different game.");
|
|
|
|
incrementUndo = true;
|
|
inputRec::log("Replaying input recording");
|
|
inputRec::consoleMultiLog({fmt::format("File: {}", inputRecordingData.GetFilename().ToUTF8()),
|
|
fmt::format("PCSX2 Version Used: {}", std::string(inputRecordingData.GetHeader().emu)),
|
|
fmt::format("Recording File Version: {}", inputRecordingData.GetHeader().version),
|
|
fmt::format("Associated Game Name or ISO Filename: {}", std::string(inputRecordingData.GetHeader().gameName)),
|
|
fmt::format("Author: {}", inputRecordingData.GetHeader().author),
|
|
fmt::format("Total Frames: {}", inputRecordingData.GetTotalFrames()),
|
|
fmt::format("Undo Count: {}", inputRecordingData.GetUndoCount())});
|
|
SetToReplayMode();
|
|
}
|
|
|
|
if (inputRecordingData.FromSaveState())
|
|
inputRec::consoleLog(fmt::format("Internal Starting Frame: {}", startingFrame));
|
|
frameCounter = 0;
|
|
initialLoad = false;
|
|
g_InputRecordingControls.Lock(startingFrame);
|
|
}
|
|
|
|
void InputRecording::FailedSavestate()
|
|
{
|
|
inputRec::consoleLog(fmt::format("{} is not compatible with this version of PCSX2", savestate.ToUTF8()));
|
|
inputRec::consoleLog(fmt::format("Original PCSX2 version used: {}", inputRecordingData.GetHeader().emu));
|
|
inputRecordingData.Close();
|
|
initialLoad = false;
|
|
state = InputRecordingMode::NotActive;
|
|
g_InputRecordingControls.Resume();
|
|
}
|
|
|
|
void InputRecording::Stop()
|
|
{
|
|
state = InputRecordingMode::NotActive;
|
|
pads[CONTROLLER_PORT_ONE].virtualPad->SetReadOnlyMode(false);
|
|
pads[CONTROLLER_PORT_TWO].virtualPad->SetReadOnlyMode(false);
|
|
incrementUndo = false;
|
|
if (inputRecordingData.Close())
|
|
inputRec::log("Input recording stopped");
|
|
}
|
|
|
|
bool InputRecording::Create(wxString fileName, const bool fromSaveState, wxString authorName)
|
|
{
|
|
if (!inputRecordingData.OpenNew(fileName, fromSaveState))
|
|
return false;
|
|
|
|
initialLoad = true;
|
|
state = InputRecordingMode::Recording;
|
|
if (fromSaveState)
|
|
{
|
|
savestate = fileName + "_SaveState.p2s";
|
|
if (wxFileExists(savestate))
|
|
wxCopyFile(savestate, savestate + ".bak", true);
|
|
StateCopy_SaveToFile(savestate);
|
|
}
|
|
else
|
|
sApp.SysExecute(g_Conf->CdvdSource);
|
|
|
|
// Set emulator version
|
|
inputRecordingData.GetHeader().SetEmulatorVersion();
|
|
|
|
// Set author name
|
|
if (!authorName.IsEmpty())
|
|
inputRecordingData.GetHeader().SetAuthor(authorName);
|
|
|
|
// Set Game Name
|
|
inputRecordingData.GetHeader().SetGameName(resolveGameName());
|
|
// Write header contents
|
|
inputRecordingData.WriteHeader();
|
|
return true;
|
|
}
|
|
|
|
bool InputRecording::Play(wxWindow* parent, wxString filename)
|
|
{
|
|
if (!inputRecordingData.OpenExisting(filename))
|
|
return false;
|
|
|
|
// Either load the savestate, or restart the game
|
|
if (inputRecordingData.FromSaveState())
|
|
{
|
|
if (!GetCoreThread().IsOpen())
|
|
{
|
|
inputRec::consoleLog("Game is not open, aborting playing input recording which starts on a save-state.");
|
|
inputRecordingData.Close();
|
|
return false;
|
|
}
|
|
|
|
savestate = inputRecordingData.GetFilename() + "_SaveState.p2s";
|
|
if (!wxFileExists(savestate))
|
|
{
|
|
wxFileDialog loadStateDialog(parent, _("Select the savestate that will accompany this recording"), L"", L"",
|
|
L"Savestate files (*.p2s)|*.p2s", wxFD_OPEN);
|
|
if (loadStateDialog.ShowModal() == wxID_CANCEL)
|
|
{
|
|
inputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}", savestate.ToUTF8()));
|
|
inputRec::log("Savestate load failed");
|
|
inputRecordingData.Close();
|
|
return false;
|
|
}
|
|
|
|
savestate = loadStateDialog.GetPath();
|
|
inputRec::consoleLog(fmt::format("Base savestate set to {}", savestate.ToUTF8()));
|
|
}
|
|
state = InputRecordingMode::Replaying;
|
|
initialLoad = true;
|
|
StateCopy_LoadFromFile(savestate);
|
|
}
|
|
else
|
|
{
|
|
state = InputRecordingMode::Replaying;
|
|
initialLoad = true;
|
|
sApp.SysExecute(g_Conf->CdvdSource);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void InputRecording::GoToFirstFrame(wxWindow* parent)
|
|
{
|
|
if (inputRecordingData.FromSaveState())
|
|
{
|
|
if (!wxFileExists(savestate))
|
|
{
|
|
const bool initiallyPaused = g_InputRecordingControls.IsPaused();
|
|
|
|
if (!initiallyPaused)
|
|
g_InputRecordingControls.PauseImmediately();
|
|
|
|
inputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}\n", savestate.ToUTF8()));
|
|
wxFileDialog loadStateDialog(parent, _("Select a savestate to accompany the recording with"), L"", L"",
|
|
L"Savestate files (*.p2s)|*.p2s", wxFD_OPEN);
|
|
int result = loadStateDialog.ShowModal();
|
|
if (!initiallyPaused)
|
|
g_InputRecordingControls.Resume();
|
|
|
|
if (result == wxID_CANCEL)
|
|
{
|
|
inputRec::log("Savestate load cancelled");
|
|
return;
|
|
}
|
|
savestate = loadStateDialog.GetPath();
|
|
inputRec::consoleLog(fmt::format ("Base savestate swapped to {}", savestate.ToUTF8()));
|
|
}
|
|
StateCopy_LoadFromFile(savestate);
|
|
}
|
|
else
|
|
sApp.SysExecute(g_Conf->CdvdSource);
|
|
|
|
if (IsRecording())
|
|
SetToReplayMode();
|
|
}
|
|
|
|
wxString InputRecording::resolveGameName()
|
|
{
|
|
std::string gameName;
|
|
const std::string gameKey(StringUtil::wxStringToUTF8String(SysGetDiscID()));
|
|
if (!gameKey.empty())
|
|
{
|
|
auto game = GameDatabase::findGame(gameKey);
|
|
if (game)
|
|
{
|
|
gameName = game->name;
|
|
gameName += " (" + game->region + ")";
|
|
}
|
|
}
|
|
return !gameName.empty() ? StringUtil::UTF8StringToWxString(gameName) : Path::GetFilename(g_Conf->CurrentIso);
|
|
}
|
|
|
|
#endif
|