Files
archived-pcsx2/pcsx2/Recording/InputRecording.cpp

406 lines
11 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Counters.h"
#include "MTGS.h"
#include "SaveState.h"
bool SaveStateBase::InputRecordingFreeze()
{
// NOTE - BE CAREFUL
// CHANGING THIS WILL BREAK BACKWARDS COMPATIBILITY ON SAVESTATES
if (!FreezeTag("InputRecording"))
return false;
Freeze(g_FrameCount);
return IsOkay();
}
#include "InputRecording.h"
#include "InputRecordingControls.h"
#include "Utilities/InputRecordingLogger.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "Counters.h"
#include "SaveState.h"
#include "VMManager.h"
#include "Host.h"
#include "ImGui/ImGuiOverlays.h"
#include "DebugTools/Debug.h"
#include "GameDatabase.h"
#include "fmt/format.h"
#include "GS.h"
#include "Host.h"
InputRecording g_InputRecording;
bool InputRecording::create(const std::string& fileName, const bool fromSaveState, const std::string& authorName)
{
if (!m_file.openNew(fileName, fromSaveState))
{
return false;
}
m_controls.setRecordMode();
if (fromSaveState)
{
std::string savestatePath = fmt::format("{}_SaveState.p2s", fileName);
if (FileSystem::FileExists(savestatePath.c_str()))
{
FileSystem::CopyFilePath(savestatePath.c_str(), fmt::format("{}.bak", savestatePath).c_str(), true);
}
m_type = Type::FROM_SAVESTATE;
m_is_active = true;
m_initial_load_complete = true;
m_watching_for_rerecords = true;
setStartingFrame(g_FrameCount);
VMManager::SaveState(savestatePath.c_str(), true, false, [](const std::string& error) {
SaveState_ReportSaveErrorOSD(error, std::nullopt);
});
}
else
{
m_starting_frame = 0;
m_type = Type::POWER_ON;
m_initial_load_complete = false;
m_is_active = true;
// TODO - should this be an explicit [full] boot instead of a reset?
VMManager::Reset();
}
m_file.setEmulatorVersion();
m_file.setAuthor(authorName);
m_file.setGameName(VMManager::GetTitle(false));
m_file.writeHeader();
initializeState();
InputRec::log(TRANSLATE_STR("InputRecording", "Started new input recording"), Host::OSD_INFO_DURATION);
InputRec::consoleLog(fmt::format("Filename {}", m_file.getFilename()));
return true;
}
bool InputRecording::play(const std::string& filename)
{
if (!m_file.openExisting(filename))
{
return false;
}
// Either load the savestate, or restart the game
if (m_file.fromSaveState())
{
std::string savestatePath = fmt::format("{}_SaveState.p2s", m_file.getFilename());
if (!FileSystem::FileExists(savestatePath.c_str()))
{
InputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}", savestatePath));
InputRec::log(TRANSLATE_STR("InputRecording", "Failed to load state for input recording"), Host::OSD_ERROR_DURATION);
m_file.close();
return false;
}
m_type = Type::FROM_SAVESTATE;
m_initial_load_complete = false;
m_is_active = true;
const auto loaded = VMManager::LoadState(savestatePath.c_str());
if (!loaded)
{
InputRec::log(TRANSLATE_STR("InputRecording", "Failed to load state for input recording, unsupported version?"), Host::OSD_ERROR_DURATION);
m_file.close();
m_is_active = false;
return false;
}
}
else
{
m_starting_frame = 0;
m_type = Type::POWER_ON;
m_initial_load_complete = false;
m_is_active = true;
// TODO - should this be an explicit [full] boot instead of a reset?
VMManager::Reset();
}
m_controls.setReplayMode();
initializeState();
InputRec::log(TRANSLATE_STR("InputRecording", "Replaying input recording"), Host::OSD_INFO_DURATION);
m_file.logRecordingMetadata();
if (VMManager::GetTitle(false) != m_file.getGameName())
{
InputRec::consoleLog(fmt::format("Input recording was possibly constructed for a different game. Expected: {}, Actual: {}", m_file.getGameName(), VMManager::GetTitle(false)));
}
return true;
}
void InputRecording::closeActiveFile()
{
if (!m_is_active)
{
return;
}
if (m_file.close())
{
m_is_active = false;
InputRec::log(TRANSLATE_STR("InputRecording", "Input recording stopped"), Host::OSD_ERROR_DURATION);
MTGS::PresentCurrentFrame();
}
else
{
InputRec::log(TRANSLATE_STR("InputRecording", "Unable to stop input recording"), Host::OSD_ERROR_DURATION);
}
}
void InputRecording::stop()
{
if (VMManager::GetState() == VMState::Paused)
{
closeActiveFile();
}
else
{
// Don't stop immediately, close the file after the current frame completes
m_recordingQueue.push([&]() {
closeActiveFile();
});
}
}
void InputRecording::handleControllerDataUpdate()
{
// TODO - multi-tap support with new file format, for now just controller 0 and 1
for (int i = 0; i < 2; i++)
{
// Fetch the current frame's data
PadData frameData(i, 0);
if (m_is_active)
{
if (m_controls.isRecording())
{
saveControllerData(frameData, i, 0);
}
else if (m_controls.isReplaying())
{
const auto& modifiedFrameData = updateControllerData(i, 0);
if (modifiedFrameData)
{
frameData = modifiedFrameData.value();
}
}
}
// Log the data we have gathered, useful for debugging our use-case
frameData.LogPadData();
}
}
void InputRecording::saveControllerData(const PadData& data, const int port, const int slot)
{
// Save the frame's data to the file
if (!m_file.writePadData(m_frame_counter, data))
{
InputRec::consoleLog(fmt::format("Failed to write input data at [{}:{}:{}]", m_frame_counter, port, slot));
}
}
std::optional<PadData> InputRecording::updateControllerData(const int port, const int slot)
{
// Get the PadData from the file
const auto frameData = m_file.readPadData(m_frame_counter, port, slot);
if (frameData)
{
// Update the g_key_status appropriately
frameData->OverrideActualController();
}
else
{
InputRec::consoleLog(fmt::format("Failed to read input data at [{}:{}:{}]", m_frame_counter, port, slot));
}
return frameData;
}
void InputRecording::processRecordQueue()
{
while (!m_recordingQueue.empty())
{
m_recordingQueue.front()();
m_recordingQueue.pop();
}
}
void InputRecording::incFrameCounter()
{
if (!m_is_active)
{
return;
}
if (m_frame_counter == std::numeric_limits<u32>::max())
{
InputRec::log(TRANSLATE_STR("InputRecording", "Congratulations, you've been playing for far too long and thus have reached the limit of input recording! Stopping recording now..."), Host::OSD_CRITICAL_ERROR_DURATION);
stop();
return;
}
m_frame_counter++;
if (m_controls.isReplaying())
{
InformGSThread();
// If we've reached the end of the recording while replaying, pause
if (m_frame_counter == m_file.getTotalFrames())
{
VMManager::SetPaused(true);
// Can also stop watching for re-records, they've watched to the end of the recording
m_watching_for_rerecords = false;
}
}
if (m_controls.isRecording())
{
m_frame_counter_stateless++;
m_file.setTotalFrames(m_frame_counter);
// If we've been in record mode and moved to the next frame, we've overrote something
// if this was following a save-state loading, this is considered a re-record, a.k.a an undo
if (m_watching_for_rerecords)
{
m_file.incrementUndoCount();
m_watching_for_rerecords = false;
}
InformGSThread();
}
}
u32 InputRecording::getFrameCounter() const
{
return m_frame_counter;
}
u32 InputRecording::getFrameCounterStateless() const
{
return m_frame_counter_stateless;
}
bool InputRecording::isActive() const
{
return m_is_active;
}
void InputRecording::handleExceededFrameCounter()
{
// if we go past the end, switch to recording mode so nothing is lost
if (m_frame_counter >= m_file.getTotalFrames() && m_controls.isReplaying())
{
m_controls.setRecordMode(false);
}
}
void InputRecording::handleReset()
{
if (m_initial_load_complete)
{
adjustFrameCounterOnReRecord(0);
}
m_initial_load_complete = true;
}
void InputRecording::handleLoadingSavestate()
{
// We need to keep track of the starting internal frame of the recording
// - For a power-on recording this should already be done - it starts at 0
// - For save state recordings, this is stored inside the initial save-state
//
// Why?
// - When you re-record you load another save-state which has it's own frame counter
// stored within, we use this to adjust the frame we are replaying/recording to
if (isTypeSavestate() && !m_initial_load_complete)
{
setStartingFrame(g_FrameCount);
m_initial_load_complete = true;
}
else
{
adjustFrameCounterOnReRecord(g_FrameCount);
m_watching_for_rerecords = true;
}
}
bool InputRecording::isTypeSavestate() const
{
return m_type == Type::FROM_SAVESTATE;
}
void InputRecording::setStartingFrame(u32 startingFrame)
{
if (m_type == Type::POWER_ON)
{
return;
}
InputRec::consoleLog(fmt::format("Internal Starting Frame: {}", startingFrame));
m_starting_frame = startingFrame;
InformGSThread();
}
u32 InputRecording::getStartingFrame()
{
return m_starting_frame;
}
void InputRecording::adjustFrameCounterOnReRecord(u32 newFrameCounter)
{
if (newFrameCounter > m_starting_frame + m_file.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("Save state's framecount has been ignored, using the max length of the recording instead.");
m_frame_counter = m_file.getTotalFrames();
if (getControls().isReplaying())
{
getControls().setRecordMode();
}
return;
}
if (newFrameCounter < m_starting_frame)
{
InputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point before the start of the original recording. This should be avoided.");
InputRec::consoleLog("Save state's framecount has been ignored, starting from the beginning in replay mode.");
m_frame_counter = 0;
if (getControls().isRecording())
{
getControls().setReplayMode();
}
return;
}
else if (newFrameCounter == 0 && getControls().isRecording())
{
getControls().setReplayMode();
}
m_frame_counter = newFrameCounter - m_starting_frame;
m_frame_counter_stateless--;
m_file.setTotalFrames(m_frame_counter);
InformGSThread();
}
InputRecordingControls& InputRecording::getControls()
{
return m_controls;
}
const InputRecordingFile& InputRecording::getData() const
{
return m_file;
}
void InputRecording::initializeState()
{
m_frame_counter = 0;
m_watching_for_rerecords = false;
InformGSThread();
}
void InputRecording::InformGSThread()
{
TinyString recording_active_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Input Recording Active: {}"), g_InputRecording.getData().getFilename());
TinyString frame_data_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Frame: {}/{} ({})"), g_InputRecording.getFrameCounter(), g_InputRecording.getData().getTotalFrames(), g_InputRecording.getFrameCounterStateless());
TinyString undo_count_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Undo Count: {}"), g_InputRecording.getData().getUndoCount());
MTGS::RunOnGSThread([recording_active_message, frame_data_message, undo_count_message](bool is_recording = g_InputRecording.getControls().isRecording()) {
g_InputRecordingData.is_recording = is_recording;
g_InputRecordingData.recording_active_message = recording_active_message;
g_InputRecordingData.frame_data_message = frame_data_message;
g_InputRecordingData.undo_count_message = undo_count_message;
});
}