mirror of
https://github.com/RetroAchievements/RAIntegration.git
synced 2026-10-12 02:09:25 +00:00
841 lines
30 KiB
C++
841 lines
30 KiB
C++
#include "MemoryBookmarksViewModel.hh"
|
|
|
|
#include "RA_Defs.h"
|
|
#include "RA_Json.h"
|
|
#include "RA_StringUtils.h"
|
|
|
|
#include "data\context\EmulatorContext.hh"
|
|
#include "data\models\TriggerValidation.hh"
|
|
|
|
#include "services\FrameEventQueue.hh"
|
|
#include "services\IConfiguration.hh"
|
|
#include "services\IFileSystem.hh"
|
|
#include "services\ILocalStorage.hh"
|
|
#include "services\SearchResults.h"
|
|
#include "services\ServiceLocator.hh"
|
|
|
|
#include "ui\viewmodels\FileDialogViewModel.hh"
|
|
#include "ui\viewmodels\MessageBoxViewModel.hh"
|
|
#include "ui\viewmodels\TriggerConditionViewModel.hh"
|
|
#include "ui\viewmodels\WindowManager.hh"
|
|
|
|
#include <rcheevos/src/rcheevos/rc_internal.h>
|
|
|
|
#ifdef RA_UTEST
|
|
// awkward workaround to allow individual bookmarks access to the bookmarks view model being tested
|
|
extern ra::ui::viewmodels::MemoryBookmarksViewModel* g_pMemoryBookmarksViewModel;
|
|
#endif
|
|
|
|
namespace ra {
|
|
namespace ui {
|
|
namespace viewmodels {
|
|
|
|
const StringModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::DescriptionProperty("MemoryBookmarkViewModel", "Description", L"");
|
|
const BoolModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::IsCustomDescriptionProperty("MemoryBookmarkViewModel", "IsCustomDescription", false);
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::AddressProperty("MemoryBookmarkViewModel", "Address", 0);
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::SizeProperty("MemoryBookmarkViewModel", "Size", ra::etoi(MemSize::EightBit));
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::FormatProperty("MemoryBookmarkViewModel", "Format", ra::etoi(MemFormat::Hex));
|
|
const StringModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::CurrentValueProperty("MemoryBookmarkViewModel", "CurrentValue", L"0");
|
|
const StringModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::PreviousValueProperty("MemoryBookmarkViewModel", "PreviousValue", L"0");
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::ChangesProperty("MemoryBookmarkViewModel", "Changes", 0);
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::BehaviorProperty("MemoryBookmarkViewModel", "Behavior", ra::etoi(BookmarkBehavior::None));
|
|
const IntModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::RowColorProperty("MemoryBookmarkViewModel", "RowColor", 0);
|
|
const BoolModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::ReadOnlyProperty("MemoryBookmarkViewModel", "IsReadOnly", false);
|
|
const BoolModelProperty MemoryBookmarksViewModel::MemoryBookmarkViewModel::IsDirtyProperty("MemoryBookmarkViewModel", "IsDirty", false);
|
|
const StringModelProperty MemoryBookmarksViewModel::FreezeButtonTextProperty("MemoryBookmarksViewModel", "FreezeButtonText", L"Freeze");
|
|
|
|
constexpr int MaxTextBookmarkLength = 8;
|
|
|
|
MemoryBookmarksViewModel::MemoryBookmarksViewModel() noexcept
|
|
{
|
|
SetWindowTitle(L"Memory Bookmarks");
|
|
|
|
m_vSizes.Add(ra::etoi(MemSize::EightBit), L" 8-bit"); // leading space for sort order
|
|
m_vSizes.Add(ra::etoi(MemSize::SixteenBit), L"16-bit");
|
|
m_vSizes.Add(ra::etoi(MemSize::TwentyFourBit), L"24-bit");
|
|
m_vSizes.Add(ra::etoi(MemSize::ThirtyTwoBit), L"32-bit");
|
|
m_vSizes.Add(ra::etoi(MemSize::SixteenBitBigEndian), L"16-bit BE");
|
|
m_vSizes.Add(ra::etoi(MemSize::TwentyFourBitBigEndian), L"24-bit BE");
|
|
m_vSizes.Add(ra::etoi(MemSize::ThirtyTwoBitBigEndian), L"32-bit BE");
|
|
m_vSizes.Add(ra::etoi(MemSize::BitCount), L"BitCount");
|
|
m_vSizes.Add(ra::etoi(MemSize::Nibble_Lower), L"Lower4");
|
|
m_vSizes.Add(ra::etoi(MemSize::Nibble_Upper), L"Upper4");
|
|
m_vSizes.Add(ra::etoi(MemSize::Float), L"Float");
|
|
m_vSizes.Add(ra::etoi(MemSize::FloatBigEndian), L"Float BE");
|
|
m_vSizes.Add(ra::etoi(MemSize::Double32), L"Double32");
|
|
m_vSizes.Add(ra::etoi(MemSize::Double32BigEndian), L"Double32 BE");
|
|
m_vSizes.Add(ra::etoi(MemSize::MBF32), L"MBF32");
|
|
m_vSizes.Add(ra::etoi(MemSize::MBF32LE), L"MBF32 LE");
|
|
m_vSizes.Add(ra::etoi(MemSize::Text), L"ASCII");
|
|
|
|
m_vFormats.Add(ra::etoi(MemFormat::Hex), L"Hex");
|
|
m_vFormats.Add(ra::etoi(MemFormat::Dec), L"Dec");
|
|
|
|
m_vBehaviors.Add(ra::etoi(BookmarkBehavior::None), L"");
|
|
m_vBehaviors.Add(ra::etoi(BookmarkBehavior::Frozen), L"Frozen");
|
|
m_vBehaviors.Add(ra::etoi(BookmarkBehavior::PauseOnChange), L"Pause");
|
|
|
|
m_vBookmarks.AddNotifyTarget(*this);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::InitializeNotifyTargets()
|
|
{
|
|
auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
|
|
pGameContext.AddNotifyTarget(*this);
|
|
|
|
auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
|
|
pEmulatorContext.AddNotifyTarget(*this);
|
|
}
|
|
|
|
GSL_SUPPRESS_F6
|
|
void MemoryBookmarksViewModel::MemoryBookmarkViewModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
|
|
{
|
|
if (args.Property == MemoryBookmarkViewModel::FormatProperty)
|
|
{
|
|
if (m_bInitialized)
|
|
{
|
|
m_bModified = true;
|
|
SetValue(CurrentValueProperty, BuildCurrentValue());
|
|
}
|
|
}
|
|
else if (args.Property == MemoryBookmarkViewModel::SizeProperty)
|
|
{
|
|
m_nSize = ra::itoe<MemSize>(args.tNewValue);
|
|
switch (m_nSize)
|
|
{
|
|
case MemSize::BitCount:
|
|
case MemSize::Text:
|
|
SetReadOnly(true);
|
|
break;
|
|
|
|
default:
|
|
SetReadOnly(false);
|
|
break;
|
|
}
|
|
|
|
if (m_bInitialized)
|
|
{
|
|
m_bModified = true;
|
|
m_nValue = ReadValue();
|
|
SetValue(CurrentValueProperty, BuildCurrentValue());
|
|
}
|
|
}
|
|
else if (args.Property == MemoryBookmarkViewModel::AddressProperty)
|
|
{
|
|
m_nAddress = args.tNewValue;
|
|
|
|
if (m_bInitialized)
|
|
{
|
|
BeginInitialization();
|
|
EndInitialization();
|
|
m_bModified = true;
|
|
}
|
|
}
|
|
else if (args.Property == MemoryBookmarkViewModel::BehaviorProperty)
|
|
{
|
|
switch (ra::itoe<BookmarkBehavior>(args.tNewValue))
|
|
{
|
|
case BookmarkBehavior::Frozen:
|
|
SetValue(RowColorProperty, 0xFFFFFFC0);
|
|
break;
|
|
|
|
default:
|
|
SetValue(RowColorProperty, MemoryBookmarkViewModel::RowColorProperty.GetDefaultValue());
|
|
break;
|
|
}
|
|
}
|
|
|
|
LookupItemViewModel::OnValueChanged(args);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MemoryBookmarkViewModel::OnValueChanged(const StringModelProperty::ChangeArgs& args)
|
|
{
|
|
if (args.Property == MemoryBookmarkViewModel::DescriptionProperty)
|
|
{
|
|
if (m_bInitialized)
|
|
{
|
|
m_bModified = true;
|
|
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes();
|
|
const auto* pNote = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNote(m_nAddress) : nullptr;
|
|
|
|
bool bIsCustomDescription = false;
|
|
if (pNote)
|
|
{
|
|
if (*pNote != args.tNewValue)
|
|
bIsCustomDescription = true;
|
|
}
|
|
else if (!args.tNewValue.empty())
|
|
{
|
|
bIsCustomDescription = true;
|
|
}
|
|
SetIsCustomDescription(bIsCustomDescription);
|
|
}
|
|
}
|
|
|
|
LookupItemViewModel::OnValueChanged(args);
|
|
}
|
|
|
|
unsigned MemoryBookmarksViewModel::MemoryBookmarkViewModel::ReadValue() const
|
|
{
|
|
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
|
|
if (m_nSize == MemSize::Text)
|
|
{
|
|
// only have 32 bits to store the value in. generate a hash for the string
|
|
std::array<uint8_t, MaxTextBookmarkLength + 1> pBuffer;
|
|
pEmulatorContext.ReadMemory(m_nAddress, &pBuffer.at(0), pBuffer.size() - 1);
|
|
pBuffer.at(pBuffer.size() - 1) = '\0';
|
|
|
|
const char* pText;
|
|
GSL_SUPPRESS_TYPE1 pText = reinterpret_cast<char*>(&pBuffer.at(0));
|
|
std::string sText(pText);
|
|
|
|
return ra::StringHash(sText);
|
|
}
|
|
|
|
return pEmulatorContext.ReadMemory(m_nAddress, m_nSize);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MemoryBookmarkViewModel::EndInitialization()
|
|
{
|
|
m_nValue = 0;
|
|
SetPreviousValue(BuildCurrentValue());
|
|
|
|
m_nValue = ReadValue();
|
|
SetValue(CurrentValueProperty, BuildCurrentValue());
|
|
|
|
SetChanges(0);
|
|
|
|
m_bModified = false;
|
|
m_bInitialized = true;
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::MemoryBookmarkViewModel::MemoryChanged()
|
|
{
|
|
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
|
|
const auto nValue = ReadValue();
|
|
if (nValue == m_nValue)
|
|
return false;
|
|
|
|
if (GetBehavior() == BookmarkBehavior::Frozen)
|
|
{
|
|
pEmulatorContext.WriteMemory(m_nAddress, m_nSize, m_nValue);
|
|
return false;
|
|
}
|
|
|
|
m_nValue = nValue;
|
|
OnValueChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MemoryBookmarkViewModel::OnValueChanged()
|
|
{
|
|
const std::wstring sValue = BuildCurrentValue();
|
|
SetPreviousValue(GetCurrentValue());
|
|
SetValue(CurrentValueProperty, sValue);
|
|
SetChanges(GetChanges() + 1);
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::MemoryBookmarkViewModel::SetCurrentValue(const std::wstring& sValue, _Out_ std::wstring& sError)
|
|
{
|
|
const auto& pEmulatorContext = ra::services::ServiceLocator::Get<ra::data::context::EmulatorContext>();
|
|
const auto nAddress = m_nAddress;
|
|
unsigned nValue = 0;
|
|
|
|
switch (m_nSize)
|
|
{
|
|
case MemSize::Float:
|
|
case MemSize::FloatBigEndian:
|
|
case MemSize::Double32:
|
|
case MemSize::Double32BigEndian:
|
|
case MemSize::MBF32:
|
|
case MemSize::MBF32LE:
|
|
float fValue;
|
|
if (!ra::ParseFloat(sValue, fValue, sError))
|
|
return false;
|
|
|
|
nValue = ra::data::FloatToU32(fValue, m_nSize);
|
|
break;
|
|
|
|
default:
|
|
const auto nMaximumValue = ra::data::MemSizeMax(m_nSize);
|
|
|
|
if (GetFormat() == MemFormat::Dec)
|
|
{
|
|
if (!ra::ParseUnsignedInt(sValue, nMaximumValue, nValue, sError))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (!ra::ParseHex(sValue, nMaximumValue, nValue, sError))
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// do not set m_nValue here, it will get set by UpdateCurrentValue which will be called by the
|
|
// OnByteWritten notification handler when WriteMemory modifies the EmulatorContext.
|
|
#ifndef RA_UTEST
|
|
auto& vmBookmarks = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::WindowManager>().MemoryBookmarks;
|
|
#else
|
|
auto& vmBookmarks = *g_pMemoryBookmarksViewModel;
|
|
#endif
|
|
vmBookmarks.BeginWritingMemory();
|
|
pEmulatorContext.WriteMemory(nAddress, m_nSize, nValue);
|
|
vmBookmarks.EndWritingMemory();
|
|
|
|
return true;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MemoryBookmarkViewModel::UpdateCurrentValue()
|
|
{
|
|
const auto nValue = ReadValue();
|
|
if (nValue != m_nValue)
|
|
{
|
|
m_nValue = nValue;
|
|
OnValueChanged();
|
|
}
|
|
}
|
|
|
|
std::wstring MemoryBookmarksViewModel::MemoryBookmarkViewModel::BuildCurrentValue() const
|
|
{
|
|
if (m_nSize == MemSize::Text)
|
|
{
|
|
ra::services::SearchResults pResults;
|
|
pResults.Initialize(m_nAddress, MaxTextBookmarkLength, ra::services::SearchType::AsciiText);
|
|
return pResults.GetFormattedValue(m_nAddress, MemSize::Text);
|
|
}
|
|
|
|
return ra::data::MemSizeFormat(m_nValue, m_nSize, GetFormat());
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::IsModified() const
|
|
{
|
|
if (m_vBookmarks.Count() != m_nUnmodifiedBookmarkCount)
|
|
return true;
|
|
|
|
for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(m_vBookmarks.Count()); ++nIndex)
|
|
{
|
|
const auto& vmBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
if (vmBookmark.IsModified())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnActiveGameChanged()
|
|
{
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
if (m_nLoadedGameId != pGameContext.GameId())
|
|
{
|
|
if (m_nLoadedGameId != 0 && IsModified())
|
|
{
|
|
auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
|
|
|
|
auto pWriter = pLocalStorage.WriteText(ra::services::StorageItemType::Bookmarks, std::to_wstring(m_nLoadedGameId));
|
|
if (pWriter != nullptr)
|
|
SaveBookmarks(*pWriter);
|
|
}
|
|
|
|
auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
|
|
|
|
auto pReader = pLocalStorage.ReadText(ra::services::StorageItemType::Bookmarks, std::to_wstring(pGameContext.GameId()));
|
|
if (pReader != nullptr)
|
|
{
|
|
LoadBookmarks(*pReader);
|
|
}
|
|
else
|
|
{
|
|
m_vBookmarks.Clear();
|
|
m_nUnmodifiedBookmarkCount = 0;
|
|
}
|
|
|
|
m_nLoadedGameId = pGameContext.GameId();
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnCodeNoteChanged(ra::ByteAddress nAddress, const std::wstring& sNewNote)
|
|
{
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
auto* pBookmark = m_vBookmarks.GetItemAt(nIndex);
|
|
if (pBookmark && pBookmark->GetAddress() == nAddress)
|
|
{
|
|
if (!pBookmark->IsCustomDescription())
|
|
{
|
|
pBookmark->SetDescription(sNewNote);
|
|
}
|
|
else
|
|
{
|
|
if (pBookmark->GetDescription() == sNewNote)
|
|
pBookmark->SetIsCustomDescription(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::LoadBookmarks(ra::services::TextReader& sBookmarksFile)
|
|
{
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
gsl::index nIndex = 0;
|
|
|
|
m_vBookmarks.BeginUpdate();
|
|
|
|
rapidjson::Document document;
|
|
if (LoadDocument(document, sBookmarksFile))
|
|
{
|
|
if (document.HasMember("Bookmarks"))
|
|
{
|
|
const auto& bookmarks = document["Bookmarks"];
|
|
for (const auto& bookmark : bookmarks.GetArray())
|
|
{
|
|
auto* vmBookmark = m_vBookmarks.GetItemAt(nIndex);
|
|
if (vmBookmark == nullptr)
|
|
{
|
|
vmBookmark = &m_vBookmarks.Add();
|
|
Ensures(vmBookmark != nullptr);
|
|
}
|
|
++nIndex;
|
|
|
|
vmBookmark->BeginInitialization();
|
|
|
|
if (bookmark.HasMember("MemAddr"))
|
|
{
|
|
// third bookmark format uses the memref serializer
|
|
uint8_t size = 0;
|
|
unsigned address = 0;
|
|
const char* memaddr = bookmark["MemAddr"].GetString();
|
|
if (rc_parse_memref(&memaddr, &size, &address) == RC_OK)
|
|
{
|
|
vmBookmark->SetAddress(address);
|
|
vmBookmark->SetSize(ra::data::models::TriggerValidation::MapRcheevosMemSize(size));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MemSize nSize = MemSize::EightBit;
|
|
|
|
if (bookmark.HasMember("Type"))
|
|
{
|
|
// original bookmark format used Type for the three supported sizes.
|
|
switch (bookmark["Type"].GetInt())
|
|
{
|
|
case 1: nSize = MemSize::EightBit; break;
|
|
case 2: nSize = MemSize::SixteenBit; break;
|
|
case 3: nSize = MemSize::ThirtyTwoBit; break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// second bookmark format used the raw enum values, which was fragile.
|
|
// this enumerates the mapping for backwards compatibility.
|
|
switch (bookmark["Size"].GetInt())
|
|
{
|
|
case 0: nSize = MemSize::Bit_0; break;
|
|
case 1: nSize = MemSize::Bit_1; break;
|
|
case 2: nSize = MemSize::Bit_2; break;
|
|
case 3: nSize = MemSize::Bit_3; break;
|
|
case 4: nSize = MemSize::Bit_4; break;
|
|
case 5: nSize = MemSize::Bit_5; break;
|
|
case 6: nSize = MemSize::Bit_6; break;
|
|
case 7: nSize = MemSize::Bit_7; break;
|
|
case 8: nSize = MemSize::Nibble_Lower; break;
|
|
case 9: nSize = MemSize::Nibble_Upper; break;
|
|
case 10: nSize = MemSize::EightBit; break;
|
|
case 11: nSize = MemSize::SixteenBit; break;
|
|
case 12: nSize = MemSize::TwentyFourBit; break;
|
|
case 13: nSize = MemSize::ThirtyTwoBit; break;
|
|
case 14: nSize = MemSize::BitCount; break;
|
|
case 15: nSize = MemSize::Text; break;
|
|
}
|
|
}
|
|
|
|
vmBookmark->SetSize(nSize);
|
|
vmBookmark->SetAddress(bookmark["Address"].GetUint());
|
|
}
|
|
|
|
const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes();
|
|
const auto* pNote = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNote(vmBookmark->GetAddress()) : nullptr;
|
|
|
|
std::wstring sDescription;
|
|
if (bookmark.HasMember("Description"))
|
|
{
|
|
sDescription = ra::Widen(bookmark["Description"].GetString());
|
|
vmBookmark->SetIsCustomDescription(!pNote || sDescription != *pNote);
|
|
}
|
|
else
|
|
{
|
|
vmBookmark->SetIsCustomDescription(false);
|
|
if (pNote)
|
|
sDescription = *pNote;
|
|
}
|
|
vmBookmark->SetDescription(sDescription);
|
|
|
|
if (bookmark.HasMember("Decimal") && bookmark["Decimal"].GetBool())
|
|
vmBookmark->SetFormat(ra::MemFormat::Dec);
|
|
else
|
|
vmBookmark->SetFormat(ra::MemFormat::Hex);
|
|
|
|
vmBookmark->SetBehavior(MemoryBookmarksViewModel::BookmarkBehavior::None);
|
|
|
|
vmBookmark->EndInitialization();
|
|
}
|
|
}
|
|
}
|
|
|
|
while (m_vBookmarks.Count() > ra::to_unsigned(nIndex))
|
|
m_vBookmarks.RemoveAt(m_vBookmarks.Count() - 1);
|
|
|
|
m_vBookmarks.EndUpdate();
|
|
|
|
m_nUnmodifiedBookmarkCount = m_vBookmarks.Count();
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::SaveBookmarks(ra::services::TextWriter& sBookmarksFile)
|
|
{
|
|
TriggerConditionViewModel vmCondition;
|
|
vmCondition.SetOperator(ra::ui::viewmodels::TriggerOperatorType::None);
|
|
|
|
rapidjson::Document document;
|
|
auto& allocator = document.GetAllocator();
|
|
document.SetObject();
|
|
|
|
rapidjson::Value bookmarks(rapidjson::kArrayType);
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
auto& vmBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
|
|
rapidjson::Value item(rapidjson::kObjectType);
|
|
|
|
const auto nSize = vmBookmark.GetSize();
|
|
switch (nSize)
|
|
{
|
|
case MemSize::Text:
|
|
item.AddMember("Size", 15, allocator);
|
|
item.AddMember("Address", vmBookmark.GetAddress(), allocator);
|
|
break;
|
|
|
|
default:
|
|
vmCondition.SetSourceSize(nSize);
|
|
vmCondition.SetSourceValue(vmBookmark.GetAddress());
|
|
item.AddMember("MemAddr", vmCondition.Serialize(), allocator);
|
|
break;
|
|
}
|
|
|
|
if (vmBookmark.GetFormat() != MemFormat::Hex)
|
|
item.AddMember("Decimal", true, allocator);
|
|
|
|
if (vmBookmark.IsCustomDescription())
|
|
item.AddMember("Description", ra::Narrow(vmBookmark.GetDescription()), allocator);
|
|
|
|
bookmarks.PushBack(item, allocator);
|
|
vmBookmark.ResetModified();
|
|
}
|
|
|
|
document.AddMember("Bookmarks", bookmarks, allocator);
|
|
SaveDocument(document, sBookmarksFile);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnByteWritten(ra::ByteAddress nAddress, uint8_t)
|
|
{
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
auto& pBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
const auto nBookmarkAddress = pBookmark.GetAddress();
|
|
if (nAddress < nBookmarkAddress)
|
|
continue;
|
|
|
|
const auto nSize = pBookmark.GetSize();
|
|
const auto nBytes = (nSize == MemSize::Text) ? MaxTextBookmarkLength : ra::data::MemSizeBytes(nSize);
|
|
if (nAddress >= nBookmarkAddress + nBytes)
|
|
continue;
|
|
|
|
if (m_nWritingMemoryCount)
|
|
pBookmark.SetDirty(true);
|
|
else
|
|
pBookmark.UpdateCurrentValue();
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::EndWritingMemory()
|
|
{
|
|
if (--m_nWritingMemoryCount == 0)
|
|
{
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
auto& pBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
if (pBookmark.IsDirty())
|
|
{
|
|
pBookmark.SetDirty(false);
|
|
pBookmark.UpdateCurrentValue();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::DoFrame()
|
|
{
|
|
auto& pEmulatorContext = ra::services::ServiceLocator::GetMutable<ra::data::context::EmulatorContext>();
|
|
pEmulatorContext.RemoveNotifyTarget(*this);
|
|
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
auto& pBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
if (pBookmark.MemoryChanged())
|
|
{
|
|
if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange)
|
|
{
|
|
pBookmark.SetRowColor(ra::ui::Color(0xFFFFC0C0));
|
|
|
|
const auto nSizeIndex = m_vSizes.FindItemIndex(LookupItemViewModel::IdProperty, ra::etoi(pBookmark.GetSize()));
|
|
Expects(nSizeIndex >= 0);
|
|
|
|
auto sMessage = ra::StringPrintf(L"%s %s",
|
|
m_vSizes.GetItemAt(nSizeIndex)->GetLabel(),
|
|
ra::ByteAddressToString(pBookmark.GetAddress()));
|
|
|
|
// remove leading space of " 8-bit"
|
|
if (isspace(sMessage.at(0)))
|
|
sMessage.erase(0, 1);
|
|
|
|
const auto& pDescription = pBookmark.GetDescription();
|
|
if (!pDescription.empty())
|
|
{
|
|
auto nDescriptionLength = pDescription.find(L'\n');
|
|
if (nDescriptionLength == std::string::npos)
|
|
{
|
|
if (pDescription.length() < 40)
|
|
{
|
|
nDescriptionLength = pDescription.length();
|
|
}
|
|
else
|
|
{
|
|
nDescriptionLength = pDescription.find_last_of(L' ', 40);
|
|
if (nDescriptionLength == std::string::npos)
|
|
nDescriptionLength = 40;
|
|
}
|
|
}
|
|
sMessage.append(L": ");
|
|
sMessage.append(pDescription, 0, nDescriptionLength);
|
|
}
|
|
|
|
auto& pFrameEventQueue = ra::services::ServiceLocator::GetMutable<ra::services::FrameEventQueue>();
|
|
pFrameEventQueue.QueuePauseOnChange(sMessage);
|
|
}
|
|
}
|
|
else if (pBookmark.GetBehavior() == BookmarkBehavior::PauseOnChange)
|
|
{
|
|
pBookmark.SetRowColor(ra::ui::Color(ra::to_unsigned(MemoryBookmarkViewModel::RowColorProperty.GetDefaultValue())));
|
|
}
|
|
}
|
|
|
|
pEmulatorContext.AddNotifyTarget(*this);
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::HasBookmark(ra::ByteAddress nAddress) const
|
|
{
|
|
return (m_vBookmarks.FindItemIndex(MemoryBookmarkViewModel::AddressProperty, nAddress) >= 0);
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::HasFrozenBookmark(ra::ByteAddress nAddress) const
|
|
{
|
|
for (size_t nIndex = 0; nIndex < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
const auto* pBookmark = m_vBookmarks.GetItemAt(nIndex);
|
|
if (pBookmark != nullptr && pBookmark->GetBehavior() == BookmarkBehavior::Frozen && pBookmark->GetAddress() == nAddress)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::AddBookmark(ra::ByteAddress nAddress, MemSize nSize)
|
|
{
|
|
auto vmBookmark = std::make_unique<MemoryBookmarkViewModel>();
|
|
vmBookmark->BeginInitialization();
|
|
vmBookmark->SetAddress(nAddress);
|
|
vmBookmark->SetSize(nSize);
|
|
|
|
const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
|
|
vmBookmark->SetFormat(pConfiguration.IsFeatureEnabled(ra::services::Feature::PreferDecimal) ? MemFormat::Dec : MemFormat::Hex);
|
|
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
const auto* pCodeNotes = pGameContext.Assets().FindCodeNotes();
|
|
const auto* pNote = (pCodeNotes != nullptr) ? pCodeNotes->FindCodeNote(nAddress) : nullptr;
|
|
if (pNote)
|
|
{
|
|
vmBookmark->SetDescription(*pNote);
|
|
|
|
// if bookmarking an 8-byte double, automatically adjust the bookmark for the significant bytes
|
|
if (nSize == MemSize::Double32 && pCodeNotes->GetCodeNoteBytes(nAddress) == 8)
|
|
vmBookmark->SetAddress(nAddress + 4);
|
|
}
|
|
|
|
vmBookmark->EndInitialization();
|
|
|
|
m_vBookmarks.Append(std::move(vmBookmark));
|
|
}
|
|
|
|
int MemoryBookmarksViewModel::RemoveSelectedBookmarks()
|
|
{
|
|
m_vBookmarks.BeginUpdate();
|
|
|
|
int nRemoved = 0;
|
|
for (gsl::index nIndex = m_vBookmarks.Count() - 1; nIndex >= 0; --nIndex)
|
|
{
|
|
if (m_vBookmarks.GetItemAt(nIndex)->IsSelected())
|
|
{
|
|
m_vBookmarks.RemoveAt(nIndex);
|
|
++nRemoved;
|
|
}
|
|
}
|
|
|
|
m_vBookmarks.EndUpdate();
|
|
|
|
return nRemoved;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnViewModelBoolValueChanged(gsl::index, const BoolModelProperty::ChangeArgs& args)
|
|
{
|
|
if (args.Property == LookupItemViewModel::IsSelectedProperty)
|
|
{
|
|
if (!m_vBookmarks.IsUpdating())
|
|
UpdateFreezeButtonText();
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnViewModelIntValueChanged(gsl::index, const IntModelProperty::ChangeArgs& args)
|
|
{
|
|
if (args.Property == MemoryBookmarkViewModel::BehaviorProperty)
|
|
{
|
|
if (!m_vBookmarks.IsUpdating())
|
|
UpdateFreezeButtonText();
|
|
}
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::OnEndViewModelCollectionUpdate()
|
|
{
|
|
UpdateFreezeButtonText();
|
|
}
|
|
|
|
bool MemoryBookmarksViewModel::ShouldFreeze() const
|
|
{
|
|
for (gsl::index nIndex = 0; ra::to_unsigned(nIndex) < m_vBookmarks.Count(); ++nIndex)
|
|
{
|
|
const auto& pBookmark = *m_vBookmarks.GetItemAt(nIndex);
|
|
if (pBookmark.IsSelected() && pBookmark.GetBehavior() != BookmarkBehavior::Frozen)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::UpdateFreezeButtonText()
|
|
{
|
|
if (ShouldFreeze())
|
|
SetValue(FreezeButtonTextProperty, FreezeButtonTextProperty.GetDefaultValue());
|
|
else
|
|
SetValue(FreezeButtonTextProperty, L"Unfreeze");
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::ToggleFreezeSelected()
|
|
{
|
|
m_vBookmarks.BeginUpdate();
|
|
|
|
if (ShouldFreeze())
|
|
{
|
|
for (gsl::index nIndex = m_vBookmarks.Count() - 1; nIndex >= 0; --nIndex)
|
|
{
|
|
auto* pItem = m_vBookmarks.GetItemAt(nIndex);
|
|
if (pItem && pItem->IsSelected())
|
|
pItem->SetBehavior(BookmarkBehavior::Frozen);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (gsl::index nIndex = m_vBookmarks.Count() - 1; nIndex >= 0; --nIndex)
|
|
{
|
|
auto* pItem = m_vBookmarks.GetItemAt(nIndex);
|
|
if (pItem && pItem->IsSelected() && pItem->GetBehavior() == BookmarkBehavior::Frozen)
|
|
pItem->SetBehavior(BookmarkBehavior::None);
|
|
}
|
|
}
|
|
|
|
m_vBookmarks.EndUpdate();
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MoveSelectedBookmarksUp()
|
|
{
|
|
m_vBookmarks.ShiftItemsUp(MemoryBookmarkViewModel::IsSelectedProperty);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::MoveSelectedBookmarksDown()
|
|
{
|
|
m_vBookmarks.ShiftItemsDown(MemoryBookmarkViewModel::IsSelectedProperty);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::ClearAllChanges()
|
|
{
|
|
for (gsl::index nIndex = m_vBookmarks.Count() - 1; nIndex >= 0; --nIndex)
|
|
m_vBookmarks.SetItemValue(nIndex, MemoryBookmarksViewModel::MemoryBookmarkViewModel::ChangesProperty, 0);
|
|
}
|
|
|
|
void MemoryBookmarksViewModel::LoadBookmarkFile()
|
|
{
|
|
ra::ui::viewmodels::FileDialogViewModel vmFileDialog;
|
|
vmFileDialog.SetWindowTitle(L"Import Bookmark File");
|
|
vmFileDialog.AddFileType(L"JSON File", L"*.json");
|
|
vmFileDialog.AddFileType(L"Text File", L"*.txt");
|
|
vmFileDialog.SetDefaultExtension(L"json");
|
|
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
vmFileDialog.SetFileName(ra::StringPrintf(L"%u-Bookmarks.json", pGameContext.GameId()));
|
|
|
|
if (vmFileDialog.ShowOpenFileDialog() == ra::ui::DialogResult::OK)
|
|
{
|
|
const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
|
|
auto pTextReader = pFileSystem.OpenTextFile(vmFileDialog.GetFileName());
|
|
if (pTextReader == nullptr)
|
|
{
|
|
ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
|
|
ra::StringPrintf(L"Could not open %s", vmFileDialog.GetFileName()));
|
|
}
|
|
else
|
|
{
|
|
LoadBookmarks(*pTextReader);
|
|
}
|
|
}}
|
|
|
|
void MemoryBookmarksViewModel::SaveBookmarkFile()
|
|
{
|
|
ra::ui::viewmodels::FileDialogViewModel vmFileDialog;
|
|
vmFileDialog.SetWindowTitle(L"Export Bookmark File");
|
|
vmFileDialog.AddFileType(L"JSON File", L"*.json");
|
|
vmFileDialog.SetDefaultExtension(L"json");
|
|
|
|
const auto& pGameContext = ra::services::ServiceLocator::Get<ra::data::context::GameContext>();
|
|
vmFileDialog.SetFileName(ra::StringPrintf(L"%u-Bookmarks.json", pGameContext.GameId()));
|
|
|
|
if (vmFileDialog.ShowSaveFileDialog() == ra::ui::DialogResult::OK)
|
|
{
|
|
const auto& pFileSystem = ra::services::ServiceLocator::Get<ra::services::IFileSystem>();
|
|
auto pTextWriter = pFileSystem.CreateTextFile(vmFileDialog.GetFileName());
|
|
if (pTextWriter == nullptr)
|
|
{
|
|
ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(
|
|
ra::StringPrintf(L"Could not create %s", vmFileDialog.GetFileName()));
|
|
}
|
|
else
|
|
{
|
|
SaveBookmarks(*pTextWriter);
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace viewmodels
|
|
} // namespace ui
|
|
} // namespace ra
|