Repository: RetroAchievements/RAIntegration
Pull request #836 (2021-11-22 23:45:27)

add new asset dialog


Patch:
diff --git a/rcheevos b/rcheevos
index 9e5ed877..4c85f985 160000
--- a/rcheevos
+++ b/rcheevos
@@ -1,1 +1,1 @@
-Subproject commit 9e5ed8775522d4b46f843c03eaddaf89686f6456
+Subproject commit 4c85f985e3a3dfa9f38c52299afc905c9194d17a
diff --git a/src/RA_Defs.cpp b/src/RA_Defs.cpp
index 8d7f8d1b..a92445f0 100644
--- a/src/RA_Defs.cpp
+++ b/src/RA_Defs.cpp
@@ -49,6 +49,15 @@ static std::wstring U32ToFloatString(unsigned nValue, char nFloatType)
     value.value.u32 = nValue;
     rc_transform_memref_value(&value, nFloatType);
 
+    if (value.value.f32 < 0.000001)
+    {
+        if (value.value.f32 > 0.0)
+            return ra::StringPrintf(L"%e", value.value.f32);
+
+        if (value.value.f32 < 0.0 && value.value.f32 > -0.000001)
+            return ra::StringPrintf(L"%e", value.value.f32);
+    }
+
     std::wstring sValue = ra::StringPrintf(L"%f", value.value.f32);
     while (sValue.back() == L'0')
         sValue.pop_back();
diff --git a/src/RA_Integration.vcxproj b/src/RA_Integration.vcxproj
index 5fcc0524..ee8f9c4e 100644
--- a/src/RA_Integration.vcxproj
+++ b/src/RA_Integration.vcxproj
@@ -109,6 +109,7 @@
     <ClCompile Include="ui\viewmodels\MemorySearchViewModel.cpp" />
     <ClCompile Include="ui\viewmodels\MemoryViewerViewModel.cpp" />
     <ClCompile Include="ui\viewmodels\MessageBoxViewModel.cpp" />
+    <ClCompile Include="ui\viewmodels\NewAssetViewModel.cpp" />
     <ClCompile Include="ui\viewmodels\OverlayAchievementsPageViewModel.cpp" />
     <ClCompile Include="ui\viewmodels\OverlayFriendsPageViewModel.cpp" />
     <ClCompile Include="ui\viewmodels\OverlayLeaderboardsPageViewModel.cpp" />
@@ -147,6 +148,7 @@
     <ClCompile Include="ui\win32\MemoryBookmarksDialog.cpp" />
     <ClCompile Include="ui\win32\MemoryInspectorDialog.cpp" />
     <ClCompile Include="ui\win32\MessageBoxDialog.cpp" />
+    <ClCompile Include="ui\win32\NewAssetDialog.cpp" />
     <ClCompile Include="ui\win32\OverlaySettingsDialog.cpp" />
     <ClCompile Include="ui\win32\OverlayWindow.cpp" />
     <ClCompile Include="ui\win32\ProgressDialog.cpp" />
@@ -285,6 +287,7 @@
     <ClInclude Include="ui\viewmodels\MemorySearchViewModel.hh" />
     <ClInclude Include="ui\viewmodels\MemoryViewerViewModel.hh" />
     <ClInclude Include="ui\viewmodels\MessageBoxViewModel.hh" />
+    <ClInclude Include="ui\viewmodels\NewAssetViewModel.hh" />
     <ClInclude Include="ui\viewmodels\OverlayAchievementsPageViewModel.hh" />
     <ClInclude Include="ui\viewmodels\OverlayFriendsPageViewModel.hh" />
     <ClInclude Include="ui\viewmodels\OverlayLeaderboardsPageViewModel.hh" />
@@ -335,6 +338,7 @@
     <ClInclude Include="ui\win32\MemoryBookmarksDialog.hh" />
     <ClInclude Include="ui\win32\MemoryInspectorDialog.hh" />
     <ClInclude Include="ui\win32\MessageBoxDialog.hh" />
+    <ClInclude Include="ui\win32\NewAssetDialog.hh" />
     <ClInclude Include="ui\win32\OverlaySettingsDialog.hh" />
     <ClInclude Include="ui\win32\OverlayWindow.hh" />
     <ClInclude Include="ui\win32\ProgressDialog.hh" />
diff --git a/src/RA_Integration.vcxproj.filters b/src/RA_Integration.vcxproj.filters
index 76704679..cf02b798 100644
--- a/src/RA_Integration.vcxproj.filters
+++ b/src/RA_Integration.vcxproj.filters
@@ -381,6 +381,12 @@
     <ClCompile Include="data\models\TriggerValidation.cpp">
       <Filter>Data\Models</Filter>
     </ClCompile>
+    <ClCompile Include="ui\win32\NewAssetDialog.cpp">
+      <Filter>UI\Win32</Filter>
+    </ClCompile>
+    <ClCompile Include="ui\viewmodels\NewAssetViewModel.cpp">
+      <Filter>UI\ViewModels</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="RA_Resource.h">
@@ -947,6 +953,12 @@
     <ClInclude Include="data\models\TriggerValidation.hh">
       <Filter>Data\Models</Filter>
     </ClInclude>
+    <ClInclude Include="ui\win32\NewAssetDialog.hh">
+      <Filter>UI\Win32</Filter>
+    </ClInclude>
+    <ClInclude Include="ui\viewmodels\NewAssetViewModel.hh">
+      <Filter>UI\ViewModels</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="RA_Shared.rc">
diff --git a/src/RA_Resource.h b/src/RA_Resource.h
index 278fa7a6..77dd9984 100644
--- a/src/RA_Resource.h
+++ b/src/RA_Resource.h
@@ -109,6 +109,18 @@
 #define IDC_RA_LBX_LBOARD_PARTS         1204
 #define IDC_RA_LBL_GROUPS               1205
 #define IDC_RA_TYPE_FILTER              1206
+#define IDC_RA_LISTACHIEVEMENTS         1207
+#define IDC_RA_CLONE_ASSET              1208
+#define IDC_RA_NEW_ASSET                1209
+#define IDC_RA_NEW_ACHIEVEMENT          1210
+#define IDC_RA_NEW_LEADERBOARD          1211
+#define IDC_RA_REVERTSELECTED           1212
+#define IDC_RA_RESET_ACH                1213
+#define IDC_RA_CHKACHPROCESSINGACTIVE   1214
+#define IDC_RA_NUMACH                   1215
+#define IDC_RA_POINT_TOTAL              1216
+#define IDC_RA_GAMEHASH                 1217
+#define IDC_RA_ADDBOOKMARK              1218
 #define IDD_RA_MEMORY                   1501
 #define IDD_RA_ACHIEVEMENTS             1502
 #define IDD_RA_ACHIEVEMENTEDITOR        1503
@@ -121,29 +133,12 @@
 #define IDD_RA_ROMCHECKSUM              1510
 #define IDD_RA_OVERLAYSETTINGS          1511
 #define IDD_RA_PROGRESS                 1512
-#define IDC_RA_ADDBOOKMARK              1518
-#define IDC_RA_HELP                     1521
-#define IDC_RA_REVERTSELECTED           1523
-#define IDC_RA_RESET_ACH                1524
-#define IDC_RA_DEL_ACH                  1530
-#define IDC_RA_CHKACHPROCESSINGACTIVE   1531
-#define IDC_RA_GOTOWIKI                 1532
-#define IDC_RA_GAMEHASH                 1533
-#define IDC_RA_NUMACH                   1534
+#define IDD_RA_NEWASSET                 1513
 #define IDC_RA_PASSWORD                 1535
-#define IDC_RA_POINT_TOTAL              1535
 #define IDC_RA_SAVEPASSWORD             1536
-#define IDC_RA_ACTIVE_CORE              1537
-#define IDC_RA_ACTIVE_UNOFFICIAL        1538
-#define IDC_RA_ACTIVE_LOCAL             1539
-#define IDC_RA_LISTACHIEVEMENTS         1540
-#define IDC_RA_ADD_ACH                  1542
-#define IDC_RA_CLONE_ACH                1544
 #define IDC_RA_USERNAME                 1549
 #define IDC_RA_DOWNLOAD_ACH             1550
 #define IDC_RA_COMMIT_ACH               1551
-#define IDC_RA_PROMOTE_ACH              1552
-#define IDC_RA_WATCH                    1557
 #define IDC_RA_GAMETITLE                1563
 #define IDC_RA_KNOWNGAMES               1565
 #define IDC_RA_CHECKSUM                 1566
@@ -163,7 +158,6 @@
 #define IDC_RA_SCANNERFOUNDINFO         1597
 #define IDC_RA_ROMCHECKSUMTEXT          1598
 #define IDC_RA_COPYCHECKSUMCLIPBOARD    1599
-#define IDC_RA_ROMCHECKSUMHEADER        1606
 #define IDM_RA_MENUSTART                1700
 #define IDM_RA_RETROACHIEVEMENTS        1700
 #define IDM_RA_OVERLAYSETTINGS          1701
@@ -191,7 +185,7 @@
 #ifndef APSTUDIO_READONLY_SYMBOLS
 #define _APS_NEXT_RESOURCE_VALUE        122
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1207
+#define _APS_NEXT_CONTROL_VALUE         1219
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif
diff --git a/src/RA_Shared.rc b/src/RA_Shared.rc
index 9f492c45..21d80025 100644
--- a/src/RA_Shared.rc
+++ b/src/RA_Shared.rc
@@ -76,8 +76,8 @@ BEGIN
     COMBOBOX        IDC_RA_SPECIAL_FILTER,4,5,54,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     COMBOBOX        IDC_RA_CATEGORY,60,5,45,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
     COMBOBOX        IDC_RA_TYPE_FILTER,107,5,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
-    LTEXT           "Game ID:",IDC_RA_GAMEID,193,7,31,8
-    RTEXT           "999999",IDC_RA_GAMEHASH,225,7,24,11
+    LTEXT           "Game ID:",IDC_RA_LBL_ID,193,7,31,8
+    RTEXT           "999999",IDC_RA_ID,225,7,24,11
     LTEXT           "Achievements:",IDC_RA_ACHIEVEMENTS,256,7,48,8
     RTEXT           "999",IDC_RA_NUMACH,304,7,12,11
     LTEXT           "Points:",IDC_RA_POINTS,322,7,23,8
@@ -89,8 +89,8 @@ BEGIN
     PUSHBUTTON      "&Save All",IDC_RA_COMMIT_ACH,367,57,68,16,BS_MULTILINE
     PUSHBUTTON      "&Reset All",IDC_RA_DOWNLOAD_ACH,367,76,68,16,BS_MULTILINE | WS_DISABLED
     PUSHBUTTON      "Re&vert All",IDC_RA_REVERTSELECTED,367,95,68,16,BS_MULTILINE
-    DEFPUSHBUTTON   "&New",IDC_RA_ADD_ACH,367,126,68,16,BS_MULTILINE | WS_DISABLED
-    PUSHBUTTON      "&Clone",IDC_RA_CLONE_ACH,367,145,68,16,BS_MULTILINE
+    DEFPUSHBUTTON   "&New",IDC_RA_NEW_ASSET,367,126,68,16,BS_MULTILINE | WS_DISABLED
+    PUSHBUTTON      "&Clone",IDC_RA_CLONE_ASSET,367,145,68,16,BS_MULTILINE
 END
 
 IDD_RA_LOGIN DIALOGEX 0, 0, 188, 132
@@ -231,7 +231,7 @@ EXSTYLE WS_EX_TOPMOST
 CAPTION "ROM Checksum"
 FONT 8, "MS Shell Dlg", 400, 0, 0x1
 BEGIN
-    LTEXT           "RetroAchievements Hash:",IDC_RA_ROMCHECKSUMHEADER,7,7,170,11
+    LTEXT           "RetroAchievements Hash:",IDC_STATIC,7,7,170,11
     LTEXT           "No game loaded.",IDC_RA_ROMCHECKSUMTEXT,7,17,170,10
     DEFPUSHBUTTON   "Copy To Clipboard",IDC_RA_COPYCHECKSUMCLIPBOARD,7,33,82,14
     DEFPUSHBUTTON   "OK",IDOK,127,33,50,14
@@ -318,8 +318,18 @@ BEGIN
     PUSHBUTTON      "Cancel",IDCANCEL,193,171,44,15
 END
 
+IDD_RA_NEWASSET DIALOGEX 0, 0, 200, 34
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU
+CAPTION "New Asset"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+    LTEXT           "What type of asset would you like to create?",IDC_STATIC,4,4,152,10
+    DEFPUSHBUTTON   "&Achievement",IDC_RA_NEW_ACHIEVEMENT,4,16,54,14
+    PUSHBUTTON      "&Leaderboard",IDC_RA_NEW_LEADERBOARD,62,16,54,14
+    PUSHBUTTON      "&Cancel",IDCANCEL,136,16,60,14
+END
 
-/////////////////////////////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////
 //
 // DESIGNINFO
 //
@@ -517,6 +527,12 @@ BEGIN
         HORZGUIDE, 107
         HORZGUIDE, 123
     END
+    
+    IDD_RA_NEWASSET, DIALOG
+    BEGIN
+        LEFTMARGIN, 4
+        TOPMARGIN, 4
+    END
 END
 #endif    // APSTUDIO_INVOKED
 
diff --git a/src/api/AwardAchievement.hh b/src/api/AwardAchievement.hh
index 4d82401d..37986429 100644
--- a/src/api/AwardAchievement.hh
+++ b/src/api/AwardAchievement.hh
@@ -15,6 +15,7 @@ public:
     struct Response : ApiResponseBase
     {
         unsigned int NewPlayerScore{ 0U };
+        unsigned int AchievementsRemaining{ 0U };
     };
 
     struct Request : ApiRequestBase
diff --git a/src/api/FetchGameData.hh b/src/api/FetchGameData.hh
index 2417aa24..f47815bf 100644
--- a/src/api/FetchGameData.hh
+++ b/src/api/FetchGameData.hh
@@ -41,6 +41,7 @@ public:
             std::string Description;
             std::string Definition;
             int Format{ 0 };
+            bool LowerIsBetter{ false };
         };
         std::vector<Leaderboard> Leaderboards;
     };
diff --git a/src/api/impl/ConnectedServer.cpp b/src/api/impl/ConnectedServer.cpp
index 9f9960ee..4ef49fda 100644
--- a/src/api/impl/ConnectedServer.cpp
+++ b/src/api/impl/ConnectedServer.cpp
@@ -18,6 +18,7 @@
 #include <rapidjson\document.h>
 
 #include <rcheevos\src\rapi\rc_api_common.h> // for parsing cached patchdata response
+#include <rc_api_editor.h>
 #include <rc_api_info.h>
 #include <rc_api_runtime.h>
 #include <rc_api_user.h>
@@ -460,7 +461,6 @@ Login::Response ConnectedServer::Login(const Login::Request& request)
 {
     Login::Response response;
 
-    std::string sPostData;
     rc_api_login_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -495,7 +495,6 @@ Login::Response ConnectedServer::Login(const Login::Request& request)
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -513,7 +512,6 @@ StartSession::Response ConnectedServer::StartSession(const StartSession::Request
 {
     StartSession::Response response;
 
-    std::string sPostData;
     rc_api_start_session_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -539,7 +537,6 @@ StartSession::Response ConnectedServer::StartSession(const StartSession::Request
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -547,7 +544,6 @@ Ping::Response ConnectedServer::Ping(const Ping::Request& request)
 {
     Ping::Response response;
 
-    std::string sPostData;
     rc_api_ping_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -580,7 +576,6 @@ Ping::Response ConnectedServer::Ping(const Ping::Request& request)
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -588,7 +583,6 @@ FetchUserUnlocks::Response ConnectedServer::FetchUserUnlocks(const FetchUserUnlo
 {
     FetchUserUnlocks::Response response;
 
-    std::string sPostData;
     rc_api_fetch_user_unlocks_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -620,7 +614,6 @@ FetchUserUnlocks::Response ConnectedServer::FetchUserUnlocks(const FetchUserUnlo
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -628,7 +621,6 @@ AwardAchievement::Response ConnectedServer::AwardAchievement(const AwardAchievem
 {
     AwardAchievement::Response response;
 
-    std::string sPostData;
     rc_api_award_achievement_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -655,6 +647,7 @@ AwardAchievement::Response ConnectedServer::AwardAchievement(const AwardAchievem
             {
                 response.Result = ApiResult::Success;
                 response.NewPlayerScore = api_response.new_player_score;
+                response.AchievementsRemaining = api_response.achievements_remaining;
             }
 
             rc_api_destroy_award_achievement_response(&api_response);
@@ -662,7 +655,6 @@ AwardAchievement::Response ConnectedServer::AwardAchievement(const AwardAchievem
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -670,7 +662,6 @@ SubmitLeaderboardEntry::Response ConnectedServer::SubmitLeaderboardEntry(const S
 {
     SubmitLeaderboardEntry::Response response;
 
-    std::string sPostData;
     rc_api_submit_lboard_entry_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -716,7 +707,6 @@ SubmitLeaderboardEntry::Response ConnectedServer::SubmitLeaderboardEntry(const S
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -764,7 +754,6 @@ ResolveHash::Response ConnectedServer::ResolveHash(const ResolveHash::Request& r
 {
     ResolveHash::Response response;
 
-    std::string sPostData;
     rc_api_resolve_hash_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -790,7 +779,6 @@ ResolveHash::Response ConnectedServer::ResolveHash(const ResolveHash::Request& r
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -798,7 +786,6 @@ FetchGameData::Response ConnectedServer::FetchGameData(const FetchGameData::Requ
 {
     FetchGameData::Response response;
 
-    std::string sPostData;
     rc_api_fetch_game_data_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
 
@@ -843,7 +830,6 @@ FetchGameData::Response ConnectedServer::FetchGameData(const FetchGameData::Requ
     }
 
     rc_api_destroy_request(&api_request);
-
     return response;
 }
 
@@ -885,6 +871,7 @@ void ConnectedServer::ProcessGamePatchData(ra::api::FetchGameData::Response &res
             pLeaderboard.Description = pSrcLeaderboard->description;
             pLeaderboard.Definition = pSrcLeaderboard->definition;
             pLeaderboard.Format = pSrcLeaderboard->format;
+            pLeaderboard.LowerIsBetter = pSrcLeaderboard->lower_is_better;
         }
     }
 
@@ -894,75 +881,108 @@ void ConnectedServer::ProcessGamePatchData(ra::api::FetchGameData::Response &res
 FetchCodeNotes::Response ConnectedServer::FetchCodeNotes(const FetchCodeNotes::Request& request)
 {
     FetchCodeNotes::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
 
-    AppendUrlParam(sPostData, "g", std::to_string(request.GameId));
+    rc_api_fetch_code_notes_request_t api_params;
+    memset(&api_params, 0, sizeof(api_params));
 
-    if (DoRequest(m_sHost, FetchCodeNotes::Name(), "codenotes2", sPostData, response, document))
+    api_params.game_id = request.GameId;
+
+    rc_api_request_t api_request;
+    if (rc_api_init_fetch_code_notes_request(&api_request, &api_params) == RC_OK)
     {
-        if (!document.HasMember("CodeNotes"))
-        {
-            response.Result = ApiResult::Error;
-            response.ErrorMessage = ra::StringPrintf("%s not found in response", "CodeNotes");
-        }
-        else
+        ra::services::Http::Response httpResponse;
+        if (DoRequest(api_request, FetchCodeNotes::Name(), httpResponse, response))
         {
-            response.Result = ApiResult::Success;
+            rc_api_fetch_code_notes_response_t api_response;
+            const auto nResult = rc_api_process_fetch_code_notes_response(&api_response, httpResponse.Content().c_str());
 
-            const auto& CodeNotes = document["CodeNotes"];
-
-            // store a copy in the cache for offline mode
-            auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
-            auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::CodeNotes, std::to_wstring(request.GameId));
-            if (pData != nullptr)
+            if (ValidateResponse(nResult, api_response.response, FetchCodeNotes::Name(), httpResponse.StatusCode(), response))
             {
-                rapidjson::Document codeNotes;
-                codeNotes.CopyFrom(CodeNotes, document.GetAllocator());
-                SaveDocument(codeNotes, *pData.get());
+                // store a copy in the cache for offline mode
+                auto& pLocalStorage = ra::services::ServiceLocator::GetMutable<ra::services::ILocalStorage>();
+                auto pData = pLocalStorage.WriteText(ra::services::StorageItemType::CodeNotes, std::to_wstring(request.GameId));
+                if (pData != nullptr)
+                {
+                    std::string sContent(httpResponse.Content());
+                    auto nIndex = sContent.find('[');
+                    sContent.erase(0, nIndex);
+                    nIndex = sContent.find_last_of(']');
+                    sContent.erase(nIndex + 1);
+                    pData->Write(sContent);
+                }
+
+                response.Result = ApiResult::Success;
+
+                ProcessCodeNotes(response, &api_response);
             }
 
-            // process it
-            ProcessCodeNotes(response, CodeNotes);
+            rc_api_destroy_fetch_code_notes_response(&api_response);
         }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
-void ConnectedServer::ProcessCodeNotes(ra::api::FetchCodeNotes::Response &response, const rapidjson::Value& CodeNotes)
+#pragma warning(push)
+#pragma warning(disable : 5045)
+void ConnectedServer::ProcessCodeNotes(FetchCodeNotes::Response& response, const void* api_response)
 {
-    for (const auto& codeNote : CodeNotes.GetArray())
+    const rc_api_fetch_code_notes_response_t* fetch_code_notes_response =
+        static_cast<const rc_api_fetch_code_notes_response_t*>(api_response);
+    const rc_api_code_note_t* note = fetch_code_notes_response->notes;
+    const rc_api_code_note_t* stop = fetch_code_notes_response->notes + fetch_code_notes_response->num_notes;
+    for (; note < stop; ++note)
     {
         auto& pNote = response.Notes.emplace_back();
-        GetRequiredJsonField(pNote.Author, codeNote, "User", response);
+        pNote.Author = note->author;
+        pNote.Address = note->address;
+        pNote.Note = ra::Widen(note->note);
+    }
+}
+#pragma warning(pop)
 
-        std::string sAddress;
-        GetRequiredJsonField(sAddress, codeNote, "Address", response);
-        pNote.Address = ra::ByteAddressFromString(sAddress);
+static void SetCodeNote(ApiResponseBase& response, const char* sApiName,
+    unsigned nGameId, ra::ByteAddress nAddress, const char* sNote)
+{
+    rc_api_update_code_note_request_t api_params;
+    memset(&api_params, 0, sizeof(api_params));
 
-        GetRequiredJsonField(pNote.Note, codeNote, "Note", response);
+    const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
+    api_params.username = pUserContext.GetUsername().c_str();
+    api_params.api_token = pUserContext.GetApiToken().c_str();
 
-        // empty notes were deleted on the server - don't bother including them in the response
-        if (pNote.Note.empty())
-            response.Notes.pop_back();
+    api_params.game_id = nGameId;
+    api_params.address = nAddress;
+    api_params.note = sNote;
+
+    rc_api_request_t api_request;
+    if (rc_api_init_update_code_note_request(&api_request, &api_params) == RC_OK)
+    {
+        ra::services::Http::Response httpResponse;
+        if (DoRequest(api_request, sApiName, httpResponse, response))
+        {
+            rc_api_update_code_note_response_t api_response;
+            const auto nResult = rc_api_process_update_code_note_response(&api_response, httpResponse.Content().c_str());
+
+            if (ValidateResponse(nResult, api_response.response, sApiName, httpResponse.StatusCode(), response))
+            {
+                response.Result = ApiResult::Success;
+            }
+
+            rc_api_destroy_update_code_note_response(&api_response);
+        }
     }
+
+    rc_api_destroy_request(&api_request);
 }
 
 UpdateCodeNote::Response ConnectedServer::UpdateCodeNote(const UpdateCodeNote::Request& request)
 {
     UpdateCodeNote::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
-
-    AppendUrlParam(sPostData, "g", std::to_string(request.GameId));
-    AppendUrlParam(sPostData, "m", std::to_string(request.Address));
-    AppendUrlParam(sPostData, "n", ra::Narrow(request.Note));
 
-    if (DoRequest(m_sHost, UpdateCodeNote::Name(), "submitcodenote", sPostData, response, document))
-    {
-        response.Result = ApiResult::Success;
-    }
+    const std::string sNote = ra::Narrow(request.Note);
+    SetCodeNote(response, UpdateCodeNote::Name(), request.GameId, request.Address, sNote.c_str());
 
     return response;
 }
@@ -970,16 +990,8 @@ UpdateCodeNote::Response ConnectedServer::UpdateCodeNote(const UpdateCodeNote::R
 DeleteCodeNote::Response ConnectedServer::DeleteCodeNote(const DeleteCodeNote::Request& request)
 {
     DeleteCodeNote::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
 
-    AppendUrlParam(sPostData, "g", std::to_string(request.GameId));
-    AppendUrlParam(sPostData, "m", std::to_string(request.Address));
-
-    if (DoRequest(m_sHost, DeleteCodeNote::Name(), "submitcodenote", sPostData, response, document))
-    {
-        response.Result = ApiResult::Success;
-    }
+    SetCodeNote(response, DeleteCodeNote::Name(), request.GameId, request.Address, nullptr);
 
     return response;
 }
@@ -987,30 +999,46 @@ DeleteCodeNote::Response ConnectedServer::DeleteCodeNote(const DeleteCodeNote::R
 UpdateAchievement::Response ConnectedServer::UpdateAchievement(const UpdateAchievement::Request& request)
 {
     UpdateAchievement::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
 
-    AppendUrlParam(sPostData, "a", std::to_string(request.AchievementId));
-    AppendUrlParam(sPostData, "g", std::to_string(request.GameId));
-    AppendUrlParam(sPostData, "n", ra::Narrow(request.Title));
-    AppendUrlParam(sPostData, "d", ra::Narrow(request.Description));
-    AppendUrlParam(sPostData, "m", request.Trigger);
-    AppendUrlParam(sPostData, "z", std::to_string(request.Points));
-    AppendUrlParam(sPostData, "f", std::to_string(request.Category));
-    AppendUrlParam(sPostData, "b", request.Badge);
+    rc_api_update_achievement_request_t api_params;
+    memset(&api_params, 0, sizeof(api_params));
 
     const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
-    const std::string sPostCode = ra::StringPrintf("%sSECRET%uSEC%s%uRE2%u",
-        pUserContext.GetUsername(), request.AchievementId, request.Trigger, request.Points, request.Points * 3);
-    const std::string sPostCodeHash = RAGenerateMD5(sPostCode);
-    AppendUrlParam(sPostData, "h", sPostCodeHash);
+    api_params.username = pUserContext.GetUsername().c_str();
+    api_params.api_token = pUserContext.GetApiToken().c_str();
+
+    const std::string sTitle = ra::Narrow(request.Title);
+    const std::string sDescription = ra::Narrow(request.Description);
 
-    if (DoRequest(m_sHost, UpdateAchievement::Name(), "uploadachievement", sPostData, response, document))
+    api_params.achievement_id = request.AchievementId;
+    api_params.game_id = request.GameId;
+    api_params.title = sTitle.c_str();
+    api_params.description = sDescription.c_str();
+    api_params.trigger = request.Trigger.c_str();
+    api_params.points = request.Points;
+    api_params.category = request.Category;
+    api_params.badge = request.Badge.c_str();
+
+    rc_api_request_t api_request;
+    if (rc_api_init_update_achievement_request(&api_request, &api_params) == RC_OK)
     {
-        response.Result = ApiResult::Success;
-        GetRequiredJsonField(response.AchievementId, document, "AchievementID", response);
+        ra::services::Http::Response httpResponse;
+        if (DoRequest(api_request, UpdateAchievement::Name(), httpResponse, response))
+        {
+            rc_api_update_achievement_response_t api_response;
+            const auto nResult = rc_api_process_update_achievement_response(&api_response, httpResponse.Content().c_str());
+
+            if (ValidateResponse(nResult, api_response.response, UpdateAchievement::Name(), httpResponse.StatusCode(), response))
+            {
+                response.Result = ApiResult::Success;
+                response.AchievementId = api_response.achievement_id;
+            }
+
+            rc_api_destroy_update_achievement_response(&api_response);
+        }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
@@ -1063,6 +1091,7 @@ FetchAchievementInfo::Response ConnectedServer::FetchAchievementInfo(const Fetch
         }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
@@ -1114,6 +1143,7 @@ FetchLeaderboardInfo::Response ConnectedServer::FetchLeaderboardInfo(const Fetch
         }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
@@ -1147,7 +1177,6 @@ LatestClient::Response ConnectedServer::LatestClient(const LatestClient::Request
 FetchGamesList::Response ConnectedServer::FetchGamesList(const FetchGamesList::Request& request)
 {
     FetchGamesList::Response response;
-    std::string sPostData;
 
     rc_api_fetch_games_list_request_t api_params;
     memset(&api_params, 0, sizeof(api_params));
@@ -1179,40 +1208,52 @@ FetchGamesList::Response ConnectedServer::FetchGamesList(const FetchGamesList::R
         }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
 SubmitNewTitle::Response ConnectedServer::SubmitNewTitle(const SubmitNewTitle::Request& request)
 {
     SubmitNewTitle::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
 
-    AppendUrlParam(sPostData, "c", std::to_string(request.ConsoleId));
-    AppendUrlParam(sPostData, "m", request.Hash);
-    AppendUrlParam(sPostData, "i", ra::Narrow(request.GameName));
+    rc_api_add_game_hash_request_t api_params;
+    memset(&api_params, 0, sizeof(api_params));
+
+    const auto& pUserContext = ra::services::ServiceLocator::Get<ra::data::context::UserContext>();
+    api_params.username = pUserContext.GetUsername().c_str();
+    api_params.api_token = pUserContext.GetApiToken().c_str();
+
+    const std::string sGameName = ra::Narrow(request.GameName);
+    const std::string sDescription = ra::Narrow(request.Description);
 
-    if (request.GameId)
-        AppendUrlParam(sPostData, "g", std::to_string(request.GameId));
+    api_params.console_id = request.ConsoleId;
+    api_params.hash = request.Hash.c_str();
+    api_params.title = sGameName.c_str();
+    api_params.game_id = request.GameId;
 
-    if (!request.Description.empty())
-        AppendUrlParam(sPostData, "d", ra::Narrow(request.Description));
+    if (!sDescription.empty())
+        api_params.hash_description = sDescription.c_str();
 
-    if (DoRequest(m_sHost, SubmitNewTitle::Name(), "submitgametitle", sPostData, response, document))
+    rc_api_request_t api_request;
+    if (rc_api_init_add_game_hash_request(&api_request, &api_params) == RC_OK)
     {
-        if (!document.HasMember("Response"))
-        {
-            response.Result = ApiResult::Error;
-            if (response.ErrorMessage.empty())
-                response.ErrorMessage = ra::StringPrintf("%s not found in response", "Response");
-        }
-        else
+        ra::services::Http::Response httpResponse;
+        if (DoRequest(api_request, SubmitNewTitle::Name(), httpResponse, response))
         {
-            response.Result = ApiResult::Success;
-            GetRequiredJsonField(response.GameId, document["Response"], "GameID", response);
+            rc_api_add_game_hash_response_t api_response;
+            const auto nResult = rc_api_process_add_game_hash_response(&api_response, httpResponse.Content().c_str());
+
+            if (ValidateResponse(nResult, api_response.response, SubmitNewTitle::Name(), httpResponse.StatusCode(), response))
+            {
+                response.Result = ApiResult::Success;
+                response.GameId = api_response.game_id;
+            }
+
+            rc_api_destroy_add_game_hash_response(&api_response);
         }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
@@ -1256,15 +1297,31 @@ SubmitTicket::Response ConnectedServer::SubmitTicket(const SubmitTicket::Request
 FetchBadgeIds::Response ConnectedServer::FetchBadgeIds(const FetchBadgeIds::Request&)
 {
     FetchBadgeIds::Response response;
-    rapidjson::Document document;
-    std::string sPostData;
 
-    if (DoRequest(m_sHost, FetchBadgeIds::Name(), "badgeiter", sPostData, response, document))
+    rc_api_fetch_badge_range_request_t api_params;
+    memset(&api_params, 0, sizeof(api_params));
+
+    rc_api_request_t api_request;
+    if (rc_api_init_fetch_badge_range_request(&api_request, &api_params) == RC_OK)
     {
-        GetRequiredJsonField(response.FirstID, document, "FirstBadge", response);
-        GetRequiredJsonField(response.NextID, document, "NextBadge", response);
+        ra::services::Http::Response httpResponse;
+        if (DoRequest(api_request, FetchBadgeIds::Name(), httpResponse, response))
+        {
+            rc_api_fetch_badge_range_response_t api_response;
+            const auto nResult = rc_api_process_fetch_badge_range_response(&api_response, httpResponse.Content().c_str());
+
+            if (ValidateResponse(nResult, api_response.response, FetchBadgeIds::Name(), httpResponse.StatusCode(), response))
+            {
+                response.Result = ApiResult::Success;
+                response.FirstID = api_response.first_badge_id;
+                response.NextID = api_response.next_badge_id;
+            }
+
+            rc_api_destroy_fetch_badge_range_response(&api_response);
+        }
     }
 
+    rc_api_destroy_request(&api_request);
     return response;
 }
 
diff --git a/src/api/impl/ConnectedServer.hh b/src/api/impl/ConnectedServer.hh
index c5ee41e5..fc09db3f 100644
--- a/src/api/impl/ConnectedServer.hh
+++ b/src/api/impl/ConnectedServer.hh
@@ -39,7 +39,7 @@ public:
     UploadBadge::Response UploadBadge(const UploadBadge::Request& request) override;
 
     static void ProcessGamePatchData(FetchGameData::Response &response, const ra::services::Http::Response& httpResponse);
-    static void ProcessCodeNotes(FetchCodeNotes::Response &response, const rapidjson::Value& CodeNotes);
+    static void ProcessCodeNotes(FetchCodeNotes::Response &response, const void* api_response);
 
 private:
     const std::string m_sHost;
diff --git a/src/api/impl/OfflineServer.cpp b/src/api/impl/OfflineServer.cpp
index 8c651f49..f028513b 100644
--- a/src/api/impl/OfflineServer.cpp
+++ b/src/api/impl/OfflineServer.cpp
@@ -7,6 +7,8 @@
 #include "services\ILocalStorage.hh"
 #include "services\ServiceLocator.hh"
 
+#include <rc_api_editor.h>
+
 namespace ra {
 namespace api {
 namespace impl {
@@ -73,7 +75,9 @@ FetchCodeNotes::Response OfflineServer::FetchCodeNotes(const FetchCodeNotes::Req
         response.Result = ApiResult::Failed;
         response.ErrorMessage = ra::StringPrintf("Code notes for game %u not found in cache", request.GameId);
     }
-    else if (!LoadDocument(document, *pData.get()))
+
+    std::string sNotes;
+    if (!pData->GetLine(sNotes)) // ASSERT: entire JSON block is a single line
     {
         response.Result = ApiResult::Error;
         response.ErrorMessage =
@@ -82,7 +86,15 @@ FetchCodeNotes::Response OfflineServer::FetchCodeNotes(const FetchCodeNotes::Req
     else
     {
         response.Result = ApiResult::Success;
-        ConnectedServer::ProcessCodeNotes(response, document);
+        sNotes.insert(0, "{\"Success\": true,\"CodeNotes\":");
+        sNotes.push_back('}');
+
+        rc_api_fetch_code_notes_response_t api_response;
+        const auto nResult = rc_api_process_fetch_code_notes_response(&api_response, sNotes.c_str());
+        if (nResult == RC_OK)
+            ConnectedServer::ProcessCodeNotes(response, &api_response);
+
+        rc_api_destroy_fetch_code_notes_response(&api_response);
     }
 
     return response;
diff --git a/src/data/context/GameAssets.cpp b/src/data/context/GameAssets.cpp
index c0e9579e..de67fbf9 100644
--- a/src/data/context/GameAssets.cpp
+++ b/src/data/context/GameAssets.cpp
@@ -42,6 +42,7 @@ ra::data::models::AchievementModel& GameAssets::NewAchievement()
 {
     auto vmAchievement = std::make_unique<ra::data::models::AchievementModel>();
     vmAchievement->SetID(m_nNextLocalId++);
+    vmAchievement->SetPoints(0);
     vmAchievement->SetNew();
     vmAchievement->CreateServerCheckpoint();
     vmAchievement->CreateLocalCheckpoint();
diff --git a/src/data/context/GameContext.cpp b/src/data/context/GameContext.cpp
index daa7b008..a1aad025 100644
--- a/src/data/context/GameContext.cpp
+++ b/src/data/context/GameContext.cpp
@@ -200,6 +200,7 @@ void GameContext::LoadGame(unsigned int nGameId, Mode nMode)
         vmLeaderboard->SetDescription(ra::Widen(pLeaderboardData.Description));
         vmLeaderboard->SetCategory(ra::data::models::AssetCategory::Core);
         vmLeaderboard->SetValueFormat(ra::itoe<ValueFormat>(pLeaderboardData.Format));
+        vmLeaderboard->SetLowerIsBetter(pLeaderboardData.LowerIsBetter);
         vmLeaderboard->SetDefinition(pLeaderboardData.Definition);
         vmLeaderboard->CreateServerCheckpoint();
         vmLeaderboard->CreateLocalCheckpoint();
@@ -375,65 +376,6 @@ void GameContext::DoFrame()
     }
 }
 
-void GameContext::AwardMastery() const
-{
-    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-
-    ra::api::FetchUserUnlocks::Request masteryRequest;
-    masteryRequest.GameId = m_nGameId;
-    masteryRequest.Hardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
-    masteryRequest.CallAsync([this](const ra::api::FetchUserUnlocks::Response& response)
-    {
-        unsigned int nNumCoreAchievements = 0;
-        unsigned int nTotalCoreAchievementPoints = 0;
-
-        bool bActiveCoreAchievement = false;
-        for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(Assets().Count()); ++nIndex)
-        {
-            const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(Assets().GetItemAt(nIndex));
-            if (pAchievement != nullptr && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core)
-            {
-                if (response.UnlockedAchievements.find(pAchievement->GetID()) == response.UnlockedAchievements.end())
-                {
-                    bActiveCoreAchievement = true;
-                    break;
-                }
-
-                ++nNumCoreAchievements;
-                nTotalCoreAchievementPoints += pAchievement->GetPoints();
-            }
-        }
-
-        if (!bActiveCoreAchievement)
-        {
-            const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
-            const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
-
-            ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\unlock.wav");
-
-            const auto nPlayTimeSeconds = ra::services::ServiceLocator::Get<ra::data::context::SessionTracker>().GetTotalPlaytime(m_nGameId);
-            const auto nPlayTimeMinutes = std::chrono::duration_cast<std::chrono::minutes>(nPlayTimeSeconds).count();
-
-            auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
-            const auto nPopup = pOverlayManager.QueueMessage(
-                ra::StringPrintf(L"%s %s", bHardcore ? L"Mastered" : L"Completed", m_sGameTitle),
-                ra::StringPrintf(L"%u achievements, %u points", nNumCoreAchievements, nTotalCoreAchievementPoints),
-                ra::StringPrintf(L"%s | Play time: %dh%02dm", pConfiguration.GetUsername(), nPlayTimeMinutes / 60, nPlayTimeMinutes % 60),
-                ra::ui::ImageType::Icon, m_sGameImage);
-
-            auto* pPopup = pOverlayManager.GetMessage(nPopup);
-            if (pPopup != nullptr)
-                pPopup->SetPopupType(ra::ui::viewmodels::Popup::Mastery);
-
-            if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot))
-            {
-                std::wstring sPath = ra::StringPrintf(L"%sGame%u.png", pConfiguration.GetScreenshotDirectory(), m_nGameId);
-                pOverlayManager.CaptureScreenshot(nPopup, sPath);
-            }
-        }
-    });
-}
-
 void GameContext::AwardAchievement(ra::AchievementID nAchievementId)
 {
     auto* pAchievement = Assets().FindAchievement(nAchievementId);
@@ -541,12 +483,15 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId)
     request.AchievementId = nAchievementId;
     request.Hardcore = _RA_HardcoreModeIsActive();
     request.GameHash = GameHash();
-    request.CallAsyncWithRetry([nPopupId, nAchievementId](const ra::api::AwardAchievement::Response& response)
+    request.CallAsyncWithRetry([this, nPopupId, nAchievementId](const ra::api::AwardAchievement::Response& response)
     {
         if (response.Succeeded())
         {
             // success! update the player's score
             ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>().SetScore(response.NewPlayerScore);
+
+            if (response.AchievementsRemaining == 0)
+                CheckForMastery();
         }
         else if (ra::StringStartsWith(response.ErrorMessage, "User already has "))
         {
@@ -591,32 +536,66 @@ void GameContext::AwardAchievement(ra::AchievementID nAchievementId)
             }
         }
     });
+}
 
-    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) != ra::ui::viewmodels::PopupLocation::None)
+void GameContext::CheckForMastery()
+{
+    const auto& pConfiguration = ra::services::ServiceLocator::Get<ra::services::IConfiguration>();
+    if (pConfiguration.GetPopupLocation(ra::ui::viewmodels::Popup::Mastery) == ra::ui::viewmodels::PopupLocation::None)
+        return;
+
+    // if multiple achievements unlock the mastery at the same time, only show the popup once
+    auto& pOverlayManager = ra::services::ServiceLocator::GetMutable<ra::ui::viewmodels::OverlayManager>();
+    if (m_nMasteryPopupId != 0 && pOverlayManager.GetMessage(m_nMasteryPopupId))
+        return;
+
+    // validate that all core assets are unlocked
+    const auto& pAssets = ra::services::ServiceLocator::Get<ra::data::context::GameContext>().Assets();
+    unsigned nTotalCoreAchievementPoints = 0;
+    size_t nNumCoreAchievements = -0;
+    bool bActiveCoreAchievement = false;
+    for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(pAssets.Count()); ++nIndex)
     {
-        bool bHasCoreAchievement = false;
-        bool bActiveCoreAchievement = false;
-        for (gsl::index nIndex = 0; nIndex < gsl::narrow_cast<gsl::index>(Assets().Count()); ++nIndex)
+        const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(pAssets.GetItemAt(nIndex));
+        if (pAchievement != nullptr && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core)
         {
-            const auto* pCheckAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(Assets().GetItemAt(nIndex));
-            if (pCheckAchievement != nullptr && pAchievement->GetCategory() == ra::data::models::AssetCategory::Core)
-            {
-                bHasCoreAchievement = true;
+            ++nNumCoreAchievements;
+            nTotalCoreAchievementPoints += pAchievement->GetPoints();
 
-                if (pCheckAchievement->IsActive())
-                {
-                    bActiveCoreAchievement = true;
-                    break;
-                }
+            if (pAchievement->IsActive())
+            {
+                bActiveCoreAchievement = true;
+                break;
             }
         }
+    }
+
+    // if they are, show the mastery notification
+    if (!bActiveCoreAchievement && nNumCoreAchievements > 0)
+    {
+        const bool bHardcore = pConfiguration.IsFeatureEnabled(ra::services::Feature::Hardcore);
+
+        const auto nPlayTimeSeconds = ra::services::ServiceLocator::Get<ra::data::context::SessionTracker>().GetTotalPlaytime(m_nGameId);
+        const auto nPlayTimeMinutes = std::chrono::duration_cast<std::chrono::minutes>(nPlayTimeSeconds).count();
+
+        std::unique_ptr<ra::ui::viewmodels::PopupMessageViewModel> vmMessage(new ra::ui::viewmodels::PopupMessageViewModel);
+        vmMessage->SetTitle(ra::StringPrintf(L"%s %s", bHardcore ? L"Mastered" : L"Completed", m_sGameTitle));
+        vmMessage->SetDescription(ra::StringPrintf(L"%u achievements, %u points", nNumCoreAchievements, nTotalCoreAchievementPoints));
+        vmMessage->SetDetail(ra::StringPrintf(L"%s | Play time: %dh%02dm", pConfiguration.GetUsername(), nPlayTimeMinutes / 60, nPlayTimeMinutes % 60));
+        vmMessage->SetImage(ra::ui::ImageType::Icon, m_sGameImage);
+        vmMessage->SetPopupType(ra::ui::viewmodels::Popup::Mastery);
+
+        // if multiple achievements unlock the mastery at the same time, only show the popup once
+        if (m_nMasteryPopupId != 0 && pOverlayManager.GetMessage(m_nMasteryPopupId))
+            return;
+
+        ra::services::ServiceLocator::Get<ra::services::IAudioSystem>().PlayAudioFile(L"Overlay\\unlock.wav");
+        m_nMasteryPopupId = pOverlayManager.QueueMessage(vmMessage);
 
-        if (!bActiveCoreAchievement && bHasCoreAchievement)
+        if (pConfiguration.IsFeatureEnabled(ra::services::Feature::MasteryNotificationScreenshot))
         {
-            // delay mastery notification by 500ms to avoid race conditions where the get unlocks API returns
-            // before the last achievement was unlocked for the user.
-            ra::services::ServiceLocator::GetMutable<ra::services::IThreadPool>()
-                .ScheduleAsync(std::chrono::milliseconds(500), [this]() { AwardMastery(); });
+            std::wstring sPath = ra::StringPrintf(L"%sGame%u.png", pConfiguration.GetScreenshotDirectory(), m_nGameId);
+            pOverlayManager.CaptureScreenshot(m_nMasteryPopupId, sPath);
         }
     }
 }
diff --git a/src/data/context/GameContext.hh b/src/data/context/GameContext.hh
index a14ff4b9..e459b742 100644
--- a/src/data/context/GameContext.hh
+++ b/src/data/context/GameContext.hh
@@ -296,6 +296,7 @@ protected:
 
 private:
     static void ExtractSize(CodeNote& pNote);
+    void CheckForMastery();
 
     /// <summary>
     /// A collection of pointers to other objects. These are not allocated object and do not need to be free'd. It's
@@ -306,6 +307,7 @@ private:
     GameAssets m_vAssets;
 
     std::atomic<int> m_nLoadCount = 0;
+    int m_nMasteryPopupId = 0;
 };
 
 } // namespace context
diff --git a/src/data/models/AchievementModel.cpp b/src/data/models/AchievementModel.cpp
index 9102d6a0..524b8dde 100644
--- a/src/data/models/AchievementModel.cpp
+++ b/src/data/models/AchievementModel.cpp
@@ -120,7 +120,7 @@ void AchievementModel::CommitTransaction()
 bool AchievementModel::ValidateAsset(std::wstring& sError)
 {
     const auto& sTrigger = GetLocalAssetDefinition(m_pTrigger);
-    return TriggerValidation::Validate(sTrigger, sError);
+    return TriggerValidation::Validate(sTrigger, sError, AssetType::Achievement);
 }
 
 void AchievementModel::DoFrame()
diff --git a/src/data/models/LeaderboardModel.cpp b/src/data/models/LeaderboardModel.cpp
index 84689dc5..def4a6b1 100644
--- a/src/data/models/LeaderboardModel.cpp
+++ b/src/data/models/LeaderboardModel.cpp
@@ -69,21 +69,21 @@ void LeaderboardModel::OnValueChanged(const IntModelProperty::ChangeArgs& args)
 bool LeaderboardModel::ValidateAsset(std::wstring& sError)
 {
     const auto& sStartTrigger = GetLocalAssetDefinition(m_pStartTrigger);
-    if (!TriggerValidation::Validate(sStartTrigger, sError))
+    if (!TriggerValidation::Validate(sStartTrigger, sError, AssetType::Leaderboard))
     {
         sError.insert(0, L"Start: ");
         return false;
     }
 
     const auto& sSubmitTrigger = GetLocalAssetDefinition(m_pSubmitTrigger);
-    if (!TriggerValidation::Validate(sSubmitTrigger, sError))
+    if (!TriggerValidation::Validate(sSubmitTrigger, sError, AssetType::Leaderboard))
     {
         sError.insert(0, L"Submit: ");
         return false;
     }
 
     const auto& sCancelTrigger = GetLocalAssetDefinition(m_pCancelTrigger);
-    if (!TriggerValidation::Validate(sCancelTrigger, sError))
+    if (!TriggerValidation::Validate(sCancelTrigger, sError, AssetType::Leaderboard))
     {
         sError.insert(0, L"Cancel: ");
         return false;
diff --git a/src/data/models/TriggerValidation.cpp b/src/data/models/TriggerValidation.cpp
index ee1367b0..537741d3 100644
--- a/src/data/models/TriggerValidation.cpp
+++ b/src/data/models/TriggerValidation.cpp
@@ -42,7 +42,46 @@ MemSize TriggerValidation::MapRcheevosMemSize(char nSize) noexcept
     }
 }
 
-bool TriggerValidation::Validate(const std::string& sTrigger, std::wstring& sError)
+static bool ValidateLeaderboardCondSet(const rc_condset_t* pCondSet, std::wstring& sError)
+{
+    const auto* pCondition = pCondSet->conditions;
+    for (; pCondition; pCondition = pCondition->next)
+    {
+        switch (pCondition->type)
+        {
+            case RC_CONDITION_MEASURED:
+                sError = ra::StringPrintf(L"%s has no effect in leaderboard triggers", L"Measured");
+                return false;
+            case RC_CONDITION_MEASURED_IF:
+                sError = ra::StringPrintf(L"%s has no effect in leaderboard triggers", L"MeasuredIf");
+                return false;
+            case RC_CONDITION_TRIGGER:
+                sError = ra::StringPrintf(L"%s has no effect in leaderboard triggers", L"Trigger");
+                return false;
+            default:
+                break;
+        }
+    }
+
+    return true;
+}
+
+static bool ValidateLeaderboardTrigger(const rc_trigger_t* pTrigger, std::wstring& sError)
+{
+    if (!ValidateLeaderboardCondSet(pTrigger->requirement, sError))
+        return false;
+
+    const auto* pCondSet = pTrigger->alternative;
+    for (; pCondSet; pCondSet = pCondSet->next)
+    {
+        if (!ValidateLeaderboardCondSet(pCondSet, sError))
+            return false;
+    }
+
+    return true;
+}
+
+bool TriggerValidation::Validate(const std::string& sTrigger, std::wstring& sError, AssetType nType)
 {
     const auto nSize = rc_trigger_size(sTrigger.c_str());
     if (nSize < 0)
@@ -65,6 +104,12 @@ bool TriggerValidation::Validate(const std::string& sTrigger, std::wstring& sErr
     char sErrorBuffer[256];
     if (rc_validate_trigger(pTrigger, sErrorBuffer, sizeof(sErrorBuffer), nMaxAddress))
     {
+        if (nType == AssetType::Leaderboard)
+        {
+            if (!ValidateLeaderboardTrigger(pTrigger, sError))
+                return false;
+        }
+
         sError.clear();
         return true;
     }
diff --git a/src/data/models/TriggerValidation.hh b/src/data/models/TriggerValidation.hh
index 6b2f9b45..2617a0d5 100644
--- a/src/data/models/TriggerValidation.hh
+++ b/src/data/models/TriggerValidation.hh
@@ -3,6 +3,7 @@
 #pragma once
 
 #include "data\Types.hh"
+#include "data\models\AssetModelBase.hh"
 
 namespace ra {
 namespace data {
@@ -11,7 +12,7 @@ namespace models {
 class TriggerValidation
 {
 public:
-    static bool Validate(const std::string& sTrigger, std::wstring& sError);
+    static bool Validate(const std::string& sTrigger, std::wstring& sError, AssetType nType);
     static MemSize MapRcheevosMemSize(char nSize) noexcept;
 };
 
diff --git a/src/rcheevos.vcxproj b/src/rcheevos.vcxproj
index 65c821db..b86baf67 100644
--- a/src/rcheevos.vcxproj
+++ b/src/rcheevos.vcxproj
@@ -182,6 +182,7 @@
     <ClCompile Include="..\rcheevos\src\rapi\rc_api_common.c">
       <PreprocessorDefinitions>_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="..\rcheevos\src\rapi\rc_api_editor.c" />
     <ClCompile Include="..\rcheevos\src\rapi\rc_api_info.c" />
     <ClCompile Include="..\rcheevos\src\rapi\rc_api_runtime.c" />
     <ClCompile Include="..\rcheevos\src\rapi\rc_api_user.c" />
diff --git a/src/rcheevos.vcxproj.filters b/src/rcheevos.vcxproj.filters
index 9578a802..366180a3 100644
--- a/src/rcheevos.vcxproj.filters
+++ b/src/rcheevos.vcxproj.filters
@@ -133,6 +133,9 @@
     <ClCompile Include="..\rcheevos\src\rcheevos\rc_validate.c">
       <Filter>rcheevos</Filter>
     </ClCompile>
+    <ClCompile Include="..\rcheevos\src\rapi\rc_api_editor.c">
+      <Filter>rapi</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <Filter Include="lua">
diff --git a/src/services/SearchResults.cpp b/src/services/SearchResults.cpp
index dc189b0f..4de184a9 100644
--- a/src/services/SearchResults.cpp
+++ b/src/services/SearchResults.cpp
@@ -1,5 +1,6 @@
 #include "SearchResults.h"
 
+#include "RA_Defs.h"
 #include "RA_StringUtils.h"
 
 #include "ra_utility.h"
@@ -10,6 +11,8 @@
 
 #include <algorithm>
 
+#include <rcheevos\src\rcheevos\rc_internal.h>
+
 // define this to use the generic filtering code for all search types
 // if defined, specialized templated code will be used for little endian searches
 #undef DISABLE_TEMPLATED_SEARCH
@@ -19,7 +22,8 @@ namespace services {
 
 namespace impl {
 
-_NODISCARD static constexpr bool CompareValues(_In_ unsigned int nLeft, _In_ unsigned int nRight, _In_ ComparisonType nCompareType) noexcept
+template<typename T>
+_NODISCARD static constexpr bool CompareValues(_In_ T nLeft, _In_ T nRight, _In_ ComparisonType nCompareType) noexcept
 {
     switch (nCompareType)
     {
@@ -93,6 +97,36 @@ public:
         return false;
     }
 
+    virtual bool ValidateFilterValue(SearchResults& srNew) const noexcept(false)
+    {
+        // convert the filter string into a value
+        if (!srNew.GetFilterString().empty())
+        {
+            const wchar_t* pStart = srNew.GetFilterString().c_str();
+            wchar_t* pEnd;
+
+            // try decimal first
+            srNew.m_nFilterValue = std::wcstoul(pStart, &pEnd, 10);
+            if (pEnd && *pEnd)
+            {
+                // decimal parse failed, try hex
+                srNew.m_nFilterValue = std::wcstoul(pStart, &pEnd, 16);
+                if (*pEnd)
+                {
+                    // hex parse failed
+                    return false;
+                }
+            }
+        }
+        else if (srNew.GetFilterType() == SearchFilterType::Constant)
+        {
+            // constant cannot be empty string
+            return false;
+        }
+
+        return true;
+    }
+
     // populates a vector of addresses that match the specified filter when applied to a previous search result
     virtual void ApplyFilter(SearchResults& srNew, const SearchResults& srPrevious) const
     {
@@ -230,13 +264,52 @@ public:
         return L"";
     }
 
-    virtual void UpdateValue(const SearchResults& pResults, SearchResults::Result& pResult,
+    virtual bool UpdateValue(const SearchResults& pResults, SearchResults::Result& pResult,
         _Out_ std::wstring* sFormattedValue, const ra::data::context::EmulatorContext& pEmulatorContext) const
     {
+        const unsigned int nPreviousValue = pResult.nValue;
         pResult.nValue = pEmulatorContext.ReadMemory(pResult.nAddress, pResult.nSize);
 
         if (sFormattedValue)
             *sFormattedValue = GetFormattedValue(pResults, pResult);
+
+        return (pResult.nValue != nPreviousValue);
+    }
+
+    virtual bool MatchesFilter(const SearchResults& pResults, const SearchResults& pPreviousResults,
+        SearchResults::Result& pResult) const noexcept(false)
+    {
+        unsigned int nPreviousValue = 0;
+        if (pResults.GetFilterType() == SearchFilterType::Constant)
+        {
+            nPreviousValue = pResults.GetFilterValue();
+        }
+        else
+        {
+            SearchResults::Result pPreviousResult{ pResult };
+            GetValue(pPreviousResults, pPreviousResult);
+
+            switch (pResults.GetFilterType())
+            {
+                case SearchFilterType::LastKnownValue:
+                case SearchFilterType::InitialValue:
+                    nPreviousValue = pPreviousResult.nValue;
+                    break;
+
+                case SearchFilterType::LastKnownValuePlus:
+                    nPreviousValue = pPreviousResult.nValue + pResults.GetFilterValue();
+                    break;
+
+                case SearchFilterType::LastKnownValueMinus:
+                    nPreviousValue = pPreviousResult.nValue - pResults.GetFilterValue();
+                    break;
+
+                default:
+                    return false;
+            }
+        }
+
+        return CompareValues(pResult.nValue, nPreviousValue, pResults.GetFilterComparison());
     }
 
 protected:
@@ -411,6 +484,11 @@ protected:
     }
 #endif
 
+    static void SetFilterValue(SearchResults& srResults, unsigned int nValue) noexcept
+    {
+        srResults.m_nFilterValue = nValue;
+    }
+
     static bool HasFirstAddress(const SearchResults& srResults) noexcept
     {
         return !srResults.m_vBlocks.empty();
@@ -608,11 +686,11 @@ class EightBitSearchImpl : public SearchImpl
 {
 protected:
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
-        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
     {
         ApplyCompareFilterLittleEndian<uint8_t, true>(pBytes, pBytesStop,
-            pPreviousBlock, nComparison, nAdjustment, vMatches);
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
     }
 
     void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
@@ -634,16 +712,16 @@ public:
     unsigned int BuildValue(const unsigned char* ptr) const noexcept override
     {
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
-        return (ptr[1] << 8) | ptr[0];
+        GSL_SUPPRESS_TYPE1 return *reinterpret_cast<const uint16_t*>(ptr);
     }
 
 protected:
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
-        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
     {
         ApplyCompareFilterLittleEndian<uint16_t, true>(pBytes, pBytesStop,
-            pPreviousBlock, nComparison, nAdjustment, vMatches);
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
     }
 
     void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
@@ -653,7 +731,6 @@ protected:
         ApplyCompareFilterLittleEndian<uint16_t, false>(pBytes, pBytesStop,
             pPreviousBlock, nComparison, nAdjustment, vMatches);
     }
-
 };
 
 class ThirtyTwoBitSearchImpl : public SearchImpl
@@ -666,16 +743,16 @@ public:
     unsigned int BuildValue(const unsigned char* ptr) const noexcept override
     {
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
-        return (ptr[3] << 24) | (ptr[2] << 16) | (ptr[1] << 8) | ptr[0];
+        GSL_SUPPRESS_TYPE1 return *reinterpret_cast<const unsigned int*>(ptr);
     }
 
 protected:
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
-        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
     {
         ApplyCompareFilterLittleEndian<uint32_t, true>(pBytes, pBytesStop,
-            pPreviousBlock, nComparison, nAdjustment, vMatches);
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
     }
 
     void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
@@ -685,7 +762,6 @@ protected:
         ApplyCompareFilterLittleEndian<uint32_t, false>(pBytes, pBytesStop,
             pPreviousBlock, nComparison, nAdjustment, vMatches);
     }
-
 };
 
 class ThirtyTwoBitAlignedSearchImpl : public ThirtyTwoBitSearchImpl
@@ -710,11 +786,11 @@ public:
 
 protected:
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
-        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
     {
         ApplyCompareFilterLittleEndian<uint32_t, true, 4>(pBytes, pBytesStop,
-            pPreviousBlock, nComparison, nAdjustment, vMatches);
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
     }
 
     void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
@@ -759,11 +835,11 @@ public:
 
 protected:
     void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
-        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
         std::vector<ra::ByteAddress>& vMatches) const override
     {
         ApplyCompareFilterLittleEndian<uint16_t, true, 2>(pBytes, pBytesStop,
-            pPreviousBlock, nComparison, nAdjustment, vMatches);
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
     }
 
     void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
@@ -795,6 +871,23 @@ class SixteenBitBigEndianSearchImpl : public SixteenBitSearchImpl
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
         return (ptr[0] << 8) | ptr[1];
     }
+
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        SearchImpl::ApplyConstantFilter(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
+    }
+
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        SearchImpl::ApplyCompareFilter(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 };
 
 class ThirtyTwoBitBigEndianSearchImpl : public ThirtyTwoBitSearchImpl
@@ -806,6 +899,23 @@ class ThirtyTwoBitBigEndianSearchImpl : public ThirtyTwoBitSearchImpl
         GSL_SUPPRESS_F6 Expects(ptr != nullptr);
         return (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3];
     }
+
+protected:
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        SearchImpl::ApplyConstantFilter(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nConstantValue, vMatches);
+    }
+
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        SearchImpl::ApplyCompareFilter(pBytes, pBytesStop,
+            pPreviousBlock, nComparison, nAdjustment, vMatches);
+    }
 };
 
 class AsciiTextSearchImpl : public SearchImpl
@@ -814,6 +924,18 @@ public:
     // indicate that search results can be very wide
     MemSize GetMemSize() const noexcept override { return MemSize::Text; }
 
+    bool ValidateFilterValue(SearchResults& srNew) const noexcept override
+    {
+        if (srNew.GetFilterString().empty())
+        {
+            // can't match 0 characters!
+            if (srNew.GetFilterType() == SearchFilterType::Constant)
+                return false;
+        }
+
+        return true;
+    }
+
     // populates a vector of addresses that match the specified filter when applied to a previous search result
     void ApplyFilter(SearchResults& srNew, const SearchResults& srPrevious) const override
     {
@@ -821,16 +943,8 @@ public:
         std::vector<unsigned char> vSearchText;
         GetSearchText(vSearchText, srNew.GetFilterString());
 
-        if (vSearchText.empty())
-        {
-            // can't match 0 characters!
-            if (srNew.GetFilterType() == SearchFilterType::Constant)
-                return;
-        }
-        else
-        {
+        if (!vSearchText.empty())
             nCompareLength = vSearchText.size();
-        }
 
         unsigned int nLargestBlock = 0U;
         for (auto& block : GetBlocks(srPrevious))
@@ -957,7 +1071,7 @@ public:
         return sText;
     }
 
-    void UpdateValue(const SearchResults&, SearchResults::Result& pResult,
+    bool UpdateValue(const SearchResults&, SearchResults::Result& pResult,
         _Out_ std::wstring* sFormattedValue, const ra::data::context::EmulatorContext& pEmulatorContext) const override
     {
         std::array<unsigned char, 16> pBuffer;
@@ -966,13 +1080,219 @@ public:
         std::wstring sText;
         GetASCIIText(sText, pBuffer.data(), pBuffer.size());
 
+        const unsigned int nPreviousValue = pResult.nValue;
         pResult.nValue = ra::StringHash(sText);
 
         if (sFormattedValue)
             sFormattedValue->swap(sText);
+
+        return (pResult.nValue != nPreviousValue);
+    }
+
+    bool MatchesFilter(const SearchResults& pResults, const SearchResults& pPreviousResults,
+        SearchResults::Result& pResult) const override
+    {
+        std::wstring sPreviousValue;
+        if (pResults.GetFilterType() == SearchFilterType::Constant)
+            sPreviousValue = pResults.GetFilterString();
+        else
+            sPreviousValue = GetFormattedValue(pPreviousResults, pResult);
+
+        const std::wstring sValue = GetFormattedValue(pResults, pResult);
+        const int nResult = wcscmp(sValue.c_str(), sPreviousValue.c_str());
+
+        switch (pResults.GetFilterComparison())
+        {
+            case ComparisonType::Equals:
+                return (nResult == 0);
+
+            case ComparisonType::NotEqualTo:
+                return (nResult != 0);
+
+            case ComparisonType::GreaterThan:
+                return (nResult > 0);
+
+            case ComparisonType::LessThan:
+                return (nResult < 0);
+
+            case ComparisonType::GreaterThanOrEqual:
+                return (nResult >= 0);
+
+            case ComparisonType::LessThanOrEqual:
+                return (nResult <= 0);
+
+            default:
+                return false;
+        }
+    }
+};
+
+class FloatSearchImpl : public ThirtyTwoBitSearchImpl
+{
+public:
+    MemSize GetMemSize() const noexcept override { return MemSize::Float; }
+
+    std::wstring GetFormattedValue(const SearchResults&, const SearchResults::Result& pResult) const override
+    {
+        return ra::data::MemSizeFormat(pResult.nValue, pResult.nSize, MemFormat::Dec);
+    }
+
+    bool ValidateFilterValue(SearchResults& srNew) const noexcept override
+    {
+        // convert the filter string into a value
+        if (!srNew.GetFilterString().empty())
+        {
+            const wchar_t* pStart = srNew.GetFilterString().c_str();
+            wchar_t* pEnd;
+
+            const float fFilterValue = std::wcstof(pStart, &pEnd);
+            if (pEnd && *pEnd)
+            {
+                // parse failed
+                return false;
+            }
+
+            SetFilterValue(srNew, DeconstructFloatValue(fFilterValue));
+        }
+        else if (srNew.GetFilterType() == SearchFilterType::Constant)
+        {
+            // constant cannot be empty string
+            return false;
+        }
+
+        return true;
+    }
+
+    GSL_SUPPRESS_TYPE1
+    bool MatchesFilter(const SearchResults& pResults, const SearchResults& pPreviousResults,
+        SearchResults::Result& pResult) const noexcept override
+    {
+        float fPreviousValue = 0.0;
+        if (pResults.GetFilterType() == SearchFilterType::Constant)
+        {
+            const auto nFilterValue = pResults.GetFilterValue();
+            const auto* pFilterValue = reinterpret_cast<const unsigned char*>(&nFilterValue);
+            fPreviousValue = BuildFloatValue(pFilterValue);
+        }
+        else
+        {
+            SearchResults::Result pPreviousResult{ pResult };
+            GetValue(pPreviousResults, pPreviousResult);
+            const auto* pPreviousValue = reinterpret_cast<const unsigned char*>(&pPreviousResult.nValue);
+            fPreviousValue = BuildFloatValue(pPreviousValue);
+        }
+
+        const auto* pValue = reinterpret_cast<const unsigned char*>(&pResult.nValue);
+        const auto fValue = BuildFloatValue(pValue);
+        return CompareValues(fValue, fPreviousValue, pResults.GetFilterComparison());
+    }
+
+protected:
+    virtual float BuildFloatValue(const unsigned char* ptr) const noexcept
+    {
+        GSL_SUPPRESS_F6 Expects(ptr != nullptr);
+        GSL_SUPPRESS_TYPE1 return *reinterpret_cast<const float*>(ptr);
+    }
+
+    virtual unsigned int DeconstructFloatValue(float fValue) const noexcept
+    {
+        return ra::data::FloatToU32(fValue, MemSize::Float);
+    }
+
+    GSL_SUPPRESS_TYPE1
+    void ApplyConstantFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nConstantValue,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        if (nComparison == ComparisonType::Equals || nComparison == ComparisonType::NotEqualTo)
+        {
+            // for direct equality, we can just compare the raw bytes without converting
+            ThirtyTwoBitSearchImpl::ApplyConstantFilter(pBytes, pBytesStop,
+                pPreviousBlock, nComparison, nConstantValue, vMatches);
+            return;
+        }
+
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto nStride = GetStride();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+
+        const auto* pConstantValue = reinterpret_cast<const unsigned char*>(&nConstantValue);
+        const float fConstantValue = BuildFloatValue(pConstantValue);
+
+        for (const auto* pScan = pBytes; pScan < pBytesStop; pScan += nStride)
+        {
+            const float fValue1 = BuildFloatValue(pScan);
+            if (CompareValues(fValue1, fConstantValue, nComparison))
+            {
+                const ra::ByteAddress nAddress = nBlockAddress +
+                    ConvertFromRealAddress(gsl::narrow_cast<unsigned int>(pScan - pBytes));
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+        }
+    }
+
+    GSL_SUPPRESS_TYPE1
+    void ApplyCompareFilter(const uint8_t* pBytes, const uint8_t* pBytesStop,
+        const MemBlock& pPreviousBlock, ComparisonType nComparison, unsigned nAdjustment,
+        std::vector<ra::ByteAddress>& vMatches) const override
+    {
+        if (nComparison == ComparisonType::Equals || nComparison == ComparisonType::NotEqualTo)
+        {
+            // for direct equality, we can just compare the raw bytes without converting
+            ThirtyTwoBitSearchImpl::ApplyCompareFilter(pBytes, pBytesStop,
+                pPreviousBlock, nComparison, nAdjustment, vMatches);
+            return;
+        }
+
+        const auto* pBlockBytes = pPreviousBlock.GetBytes();
+        const auto nBlockAddress = pPreviousBlock.GetFirstAddress();
+        const auto nStride = GetStride();
+        const auto* pMatchingAddresses = pPreviousBlock.GetMatchingAddressPointer();
+
+        const auto* pAdjustment = reinterpret_cast<const unsigned char*>(&nAdjustment);
+        const float fAdjustment = BuildFloatValue(pAdjustment);
+
+        for (const auto* pScan = pBytes; pScan < pBytesStop; pScan += nStride, pBlockBytes += nStride)
+        {
+            const float fValue1 = BuildFloatValue(pScan);
+            const float fValue2 = BuildFloatValue(pBlockBytes) + fAdjustment;
+            if (CompareValues(fValue1, fValue2, nComparison))
+            {
+                const ra::ByteAddress nAddress = nBlockAddress +
+                    ConvertFromRealAddress(gsl::narrow_cast<unsigned int>(pScan - pBytes));
+                if (pPreviousBlock.HasMatchingAddress(pMatchingAddresses, nAddress))
+                    vMatches.push_back(nAddress);
+            }
+        }
+    }
+};
+
+class MBF32SearchImpl : public FloatSearchImpl
+{
+public:
+    MemSize GetMemSize() const noexcept override { return MemSize::MBF32; }
+
+protected:
+    GSL_SUPPRESS_TYPE1
+    float BuildFloatValue(const unsigned char* ptr) const noexcept override
+    {
+        GSL_SUPPRESS_F6 Expects(ptr != nullptr);
+        rc_typed_value_t value;
+        value.type = RC_VALUE_TYPE_UNSIGNED;
+        value.value.u32 = *reinterpret_cast<const unsigned int*>(ptr);
+        rc_transform_memref_value(&value, RC_MEMSIZE_MBF32);
+
+        return value.value.f32;
+    }
+
+    unsigned int DeconstructFloatValue(float fValue) const noexcept override
+    {
+        return ra::data::FloatToU32(fValue, MemSize::MBF32);
     }
 };
 
+
 static FourBitSearchImpl s_pFourBitSearchImpl;
 static EightBitSearchImpl s_pEightBitSearchImpl;
 static SixteenBitSearchImpl s_pSixteenBitSearchImpl;
@@ -982,6 +1302,8 @@ static ThirtyTwoBitAlignedSearchImpl s_pThirtyTwoBitAlignedSearchImpl;
 static SixteenBitBigEndianSearchImpl s_pSixteenBitBigEndianSearchImpl;
 static ThirtyTwoBitBigEndianSearchImpl s_pThirtyTwoBitBigEndianSearchImpl;
 static AsciiTextSearchImpl s_pAsciiTextSearchImpl;
+static FloatSearchImpl s_pFloatSearchImpl;
+static MBF32SearchImpl s_pMBF32SearchImpl;
 
 uint8_t* MemBlock::AllocateMatchingAddresses() noexcept
 {
@@ -1166,6 +1488,12 @@ void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchTy
         case SearchType::AsciiText:
             m_pImpl = &ra::services::impl::s_pAsciiTextSearchImpl;
             break;
+        case SearchType::Float:
+            m_pImpl = &ra::services::impl::s_pFloatSearchImpl;
+            break;
+        case SearchType::MBF32:
+            m_pImpl = &ra::services::impl::s_pMBF32SearchImpl;
+            break;
     }
 
     const unsigned int nPadding = m_pImpl->GetPadding();
@@ -1187,11 +1515,6 @@ void SearchResults::Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchTy
     }
 }
 
-bool SearchResults::Result::Compare(unsigned int nPreviousValue, ComparisonType nCompareType) const noexcept
-{
-    return impl::CompareValues(nValue, nPreviousValue, nCompareType);
-}
-
 bool SearchResults::ContainsAddress(ra::ByteAddress nAddress) const
 {
     if (m_pImpl)
@@ -1200,38 +1523,6 @@ bool SearchResults::ContainsAddress(ra::ByteAddress nAddress) const
     return false;
 }
 
-_Use_decl_annotations_
-void SearchResults::Initialize(const SearchResults& srSource, ComparisonType nCompareType,
-    SearchFilterType nFilterType, unsigned int nFilterValue)
-{
-    m_nType = srSource.m_nType;
-    m_pImpl = srSource.m_pImpl;
-    m_nCompareType = nCompareType;
-    m_nFilterType = nFilterType;
-    m_nFilterValue = nFilterValue;
-
-    m_pImpl->ApplyFilter(*this, srSource);
-}
-
-_Use_decl_annotations_
-void SearchResults::Initialize(const SearchResults& srMemory, const SearchResults& srAddresses,
-    ComparisonType nCompareType, SearchFilterType nFilterType, unsigned int nFilterValue)
-{
-    if (&srMemory == &srAddresses)
-    {
-        Initialize(srMemory, nCompareType, nFilterType, nFilterValue);
-        return;
-    }
-
-    // create a new merged SearchResults object that has the matching addresses from srAddresses,
-    // and the memory blocks from srMemory.
-    SearchResults srMerge;
-    srMerge.MergeSearchResults(srMemory, srAddresses);
-
-    // then do a standard comparison against the merged SearchResults
-    Initialize(srMerge, nCompareType, nFilterType, nFilterValue);
-}
-
 void SearchResults::MergeSearchResults(const SearchResults& srMemory, const SearchResults& srAddresses)
 {
     m_vBlocks.reserve(srAddresses.m_vBlocks.size());
@@ -1273,7 +1564,7 @@ void SearchResults::MergeSearchResults(const SearchResults& srMemory, const Sear
 }
 
 _Use_decl_annotations_
-void SearchResults::Initialize(const SearchResults& srSource, ComparisonType nCompareType,
+bool SearchResults::Initialize(const SearchResults& srSource, ComparisonType nCompareType,
     SearchFilterType nFilterType, const std::wstring& sFilterValue)
 {
     m_nType = srSource.m_nType;
@@ -1282,18 +1573,19 @@ void SearchResults::Initialize(const SearchResults& srSource, ComparisonType nCo
     m_nFilterType = nFilterType;
     m_sFilterValue = sFilterValue;
 
+    if (!m_pImpl->ValidateFilterValue(*this))
+        return false;
+
     m_pImpl->ApplyFilter(*this, srSource);
+    return true;
 }
 
 _Use_decl_annotations_
-void SearchResults::Initialize(const SearchResults& srMemory, const SearchResults& srAddresses,
+bool SearchResults::Initialize(const SearchResults& srMemory, const SearchResults& srAddresses,
     ComparisonType nCompareType, SearchFilterType nFilterType, const std::wstring& sFilterValue)
 {
     if (&srMemory == &srAddresses)
-    {
-        Initialize(srMemory, nCompareType, nFilterType, sFilterValue);
-        return;
-    }
+        return Initialize(srMemory, nCompareType, nFilterType, sFilterValue);
 
     // create a new merged SearchResults object that has the matching addresses from srAddresses,
     // and the memory blocks from srMemory.
@@ -1301,7 +1593,7 @@ void SearchResults::Initialize(const SearchResults& srMemory, const SearchResult
     srMerge.MergeSearchResults(srMemory, srAddresses);
 
     // then do a standard comparison against the merged SearchResults
-    Initialize(srMerge, nCompareType, nFilterType, sFilterValue);
+    return Initialize(srMerge, nCompareType, nFilterType, sFilterValue);
 }
 
 size_t SearchResults::MatchingAddressCount() const noexcept
@@ -1339,6 +1631,16 @@ bool SearchResults::GetMatchingAddress(gsl::index nIndex, _Out_ SearchResults::R
     return m_pImpl->GetMatchingAddress(*this, nIndex, result);
 }
 
+bool SearchResults::GetMatchingAddress(const SearchResults::Result& pSrcResult, _Out_ SearchResults::Result& result) const noexcept
+{
+    memcpy(&result, &pSrcResult, sizeof(SearchResults::Result));
+
+    if (m_pImpl == nullptr)
+        return false;
+
+    return m_pImpl->GetValue(*this, result);
+}
+
 bool SearchResults::GetBytes(ra::ByteAddress nAddress, unsigned char* pBuffer, size_t nCount) const noexcept
 {
     if (m_pImpl != nullptr)
@@ -1375,14 +1677,24 @@ std::wstring SearchResults::GetFormattedValue(ra::ByteAddress nAddress, MemSize
     return m_pImpl ? m_pImpl->GetFormattedValue(*this, nAddress, nSize) : L"";
 }
 
-void SearchResults::UpdateValue(SearchResults::Result& pResult, _Out_ std::wstring* sFormattedValue,
-    const ra::data::context::EmulatorContext& pEmulatorContext) const
+bool SearchResults::UpdateValue(SearchResults::Result& pResult,
+    _Out_ std::wstring* sFormattedValue, const ra::data::context::EmulatorContext& pEmulatorContext) const
 {
     if (m_pImpl)
         return m_pImpl->UpdateValue(*this, pResult, sFormattedValue, pEmulatorContext);
 
     if (sFormattedValue)
         sFormattedValue->clear();
+
+    return false;
+}
+
+bool SearchResults::MatchesFilter(const SearchResults& pPreviousResults, SearchResults::Result& pResult) const
+{
+    if (m_pImpl)
+        return m_pImpl->MatchesFilter(*this, pPreviousResults, pResult);
+
+    return true;
 }
 
 MemSize SearchResults::GetSize() const noexcept
diff --git a/src/services/SearchResults.h b/src/services/SearchResults.h
index da71815f..e57c59c7 100644
--- a/src/services/SearchResults.h
+++ b/src/services/SearchResults.h
@@ -19,6 +19,8 @@ enum class SearchType
     ThirtyTwoBitAligned,
     SixteenBitBigEndian,
     ThirtyTwoBitBigEndian,
+    Float,
+    MBF32,
     AsciiText,
 };
 
@@ -185,27 +187,6 @@ public:
     /// <param name="nType">Type of search to initialize.</param>
     void Initialize(ra::ByteAddress nAddress, size_t nBytes, SearchType nType);
 
-    /// <summary>
-    /// Initializes a result set by comparing against another result set.
-    /// </summary>
-    /// <param name="srSource">The result set to filter.</param>
-    /// <param name="nCompareType">Type of comparison to apply.</param>
-    /// <param name="nFilterType">Type of filter to apply.</param>
-    /// <param name="nFilterValue">Parameter for filter being applied.</param>
-    void Initialize(_In_ const SearchResults& srSource, _In_ ComparisonType nCompareType,
-        _In_ SearchFilterType nFilterType, _In_ unsigned int nFilterValue);
-
-    /// <summary>
-    /// Initializes a result set by comparing against another result set using the address filter from a third set.
-    /// </summary>
-    /// <param name="srSource">The result set to filter.</param>
-    /// <param name="srAddresses">The result set specifying which addresses to examine.</param>
-    /// <param name="nCompareType">Type of comparison to apply.</param>
-    /// <param name="nFilterType">Type of filter to apply.</param>
-    /// <param name="nFilterValue">Parameter for filter being applied.</param>
-    void Initialize(_In_ const SearchResults& srSource, _In_ const SearchResults& srAddresses,
-        _In_ ComparisonType nCompareType, _In_ SearchFilterType nFilterType, _In_ unsigned int nFilterValue);
-
     /// <summary>
     /// Initializes a result set by comparing against another result set.
     /// </summary>
@@ -213,7 +194,8 @@ public:
     /// <param name="nCompareType">Type of comparison to apply.</param>
     /// <param name="nFilterType">Type of filter to apply.</param>
     /// <param name="sFilterValue">Parameter for filter being applied.</param>
-    void Initialize(_In_ const SearchResults& srSource, _In_ ComparisonType nCompareType,
+    /// <returns><c>true</c> if initialization was successful, <c>false</c> if the filter value was not supported</returns>
+    bool Initialize(_In_ const SearchResults& srSource, _In_ ComparisonType nCompareType,
         _In_ SearchFilterType nFilterType, _In_ const std::wstring& sFilterValue);
 
     /// <summary>
@@ -224,7 +206,8 @@ public:
     /// <param name="nCompareType">Type of comparison to apply.</param>
     /// <param name="nFilterType">Type of filter to apply.</param>
     /// <param name="sFilterValue">Parameter for filter being applied.</param>
-    void Initialize(_In_ const SearchResults& srSource, _In_ const SearchResults& srAddresses,
+    /// <returns><c>true</c> if initialization was successful, <c>false</c> if the filter value was not supported</returns>
+    bool Initialize(_In_ const SearchResults& srSource, _In_ const SearchResults& srAddresses,
         _In_ ComparisonType nCompareType, _In_ SearchFilterType nFilterType, _In_ const std::wstring& sFilterValue);
 
     /// <summary>
@@ -237,9 +220,6 @@ public:
         ra::ByteAddress nAddress{};
         unsigned int nValue{};
         MemSize nSize{};
-
-        bool Compare(unsigned int nPreviousValue, ComparisonType nCompareType) const noexcept;
-        bool Compare(const std::wstring& sPreviousValue, ComparisonType nCompareType) const noexcept;
     };
 
     /// <summary>
@@ -249,6 +229,14 @@ public:
     /// <returns><c>true</c> if result was populated, <c>false</c> if the index was invalid.</returns>
     bool GetMatchingAddress(gsl::index nIndex, _Out_ Result& result) const noexcept;
 
+    /// <summary>
+    /// Gets and item from the results matching a result from another set of results.
+    /// </summary>
+    /// <param name="pSrcResult">The result to find a match for.</param>
+    /// <param name="result">The result.</param>
+    /// <returns><c>true</c> if result was populated, <c>false</c> if the index was invalid.</returns>
+    bool GetMatchingAddress(const SearchResults::Result& pSrcResult, _Out_ Result& result) const noexcept;
+
     /// <summary>
     /// Gets the raw bytes at the specified address.
     /// </summary>
@@ -266,10 +254,20 @@ public:
     /// Updates the current value of the provided result.
     /// </summary>
     /// <param name="pResult">The result to update.</param>
-    /// <param name="sFormattedValue">Pointer to a string to populate with a textual representation of the value.</param>
-    void UpdateValue(SearchResults::Result& pResult, _Out_ std::wstring* sFormattedValue,
+    /// <param name="pPreviousResult">The previous value of the result.</param>
+    /// <param name="sFormattedValue">Pointer to a string to populate with a textual representation of the value. [optional]</param>
+    /// <returns><c>true</c> if the value changed, <c>false</c> if not.</returns>
+    bool UpdateValue(SearchResults::Result& pResult, _Out_ std::wstring* sFormattedValue,
         const ra::data::context::EmulatorContext& pEmulatorContext) const;
 
+    /// <summary>
+    /// Determines if a value compared to its previous value matches the filter.
+    /// </summary>
+    /// <param name="pPreviousResults">The previous results to compare against.</param>
+    /// <param name="pResult">The result to test.</param>
+    /// <returns><c>true</c> if the value still matches the filter, <c>false</c> if not.</returns>
+    bool MatchesFilter(const SearchResults& pPreviousResults, SearchResults::Result& pResult) const;
+
     /// <summary>
     /// Gets the type of search performed.
     /// </summary>
diff --git a/src/ui/viewmodels/AssetListViewModel.cpp b/src/ui/viewmodels/AssetListViewModel.cpp
index ff8647e6..0f529d07 100644
--- a/src/ui/viewmodels/AssetListViewModel.cpp
+++ b/src/ui/viewmodels/AssetListViewModel.cpp
@@ -11,6 +11,7 @@
 
 #include "ui\viewmodels\AssetUploadViewModel.hh"
 #include "ui\viewmodels\MessageBoxViewModel.hh"
+#include "ui\viewmodels\NewAssetViewModel.hh"
 #include "ui\viewmodels\OverlayManager.hh"
 #include "ui\viewmodels\WindowManager.hh"
 
@@ -1120,19 +1121,71 @@ static std::wstring ValidateTriggerLogic(const std::string& sTrigger)
     return sError;
 }
 
+static std::wstring ValidateValueLogic(const std::string& sValue)
+{
+    const auto nSize = rc_value_size(sValue.c_str());
+    if (nSize < 0)
+        return ra::StringPrintf(L"Parse Error %d: %s", nSize, rc_error_str(nSize));
+
+    std::string sBuffer;
+    sBuffer.resize(nSize);
+    const auto* pValue = rc_parse_value(sBuffer.data(), sValue.c_str(), nullptr, 0);
+
+    std::wstring sError;
+    const auto* pCondSet = pValue->conditions;
+    while (pCondSet != nullptr)
+    {
+        sError = ValidateCondSet(pCondSet);
+        if (!sError.empty())
+            break;
+
+        pCondSet = pCondSet->next;
+    }
+
+    return sError;
+}
+
 void AssetListViewModel::ValidateAchievementForCore(std::wstring& sError, const ra::data::models::AchievementModel& pAchievement) const
 {
     if (!ra::services::ServiceLocator::Get<ra::services::IConfiguration>().IsCustomHost())
     {
-        std::wstring sTriggerError = ValidateTriggerLogic(pAchievement.GetTrigger());
+        const std::wstring sTriggerError = ValidateTriggerLogic(pAchievement.GetTrigger());
         if (!sTriggerError.empty())
             sError.append(ra::StringPrintf(L"\n* %s: %s", pAchievement.GetName(), sTriggerError));
     }
 }
+
+void AssetListViewModel::ValidateLeaderboardForCore(std::wstring& sError, const ra::data::models::LeaderboardModel& pLeaderboard) const
+{
+    if (!ra::services::ServiceLocator::Get<ra::services::IConfiguration>().IsCustomHost())
+    {
+        std::wstring sTriggerError = ValidateTriggerLogic(pLeaderboard.GetStartTrigger());
+        if (!sTriggerError.empty())
+            sError.append(ra::StringPrintf(L"\n* %s: %s: %s", pLeaderboard.GetName(), L"Start", sTriggerError));
+
+        sTriggerError = ValidateTriggerLogic(pLeaderboard.GetSubmitTrigger());
+        if (!sTriggerError.empty())
+            sError.append(ra::StringPrintf(L"\n* %s: %s: %s", pLeaderboard.GetName(), L"Submit", sTriggerError));
+
+        sTriggerError = ValidateTriggerLogic(pLeaderboard.GetCancelTrigger());
+        if (!sTriggerError.empty())
+            sError.append(ra::StringPrintf(L"\n* %s: %s: %s", pLeaderboard.GetName(), L"Cancel", sTriggerError));
+
+        const std::wstring sValueError = ValidateValueLogic(pLeaderboard.GetValueDefinition());
+        if (!sValueError.empty())
+            sError.append(ra::StringPrintf(L"\n* %s: %s: %s", pLeaderboard.GetName(), L"Value", sValueError));
+    }
+
+    sError.append(ra::StringPrintf(L"\n* %s: %s", pLeaderboard.GetName(), L"Leaderboards cannot be published at this time."));
+}
 #else
 void AssetListViewModel::ValidateAchievementForCore(_UNUSED std::wstring& sError, _UNUSED const ra::data::models::AchievementModel& pAchievement) const
 {
 }
+
+void AssetListViewModel::ValidateLeaderboardForCore(_UNUSED std::wstring& sError, _UNUSED const ra::data::models::LeaderboardModel& pAchievement) const
+{
+}
 #endif
 
 bool AssetListViewModel::ValidateAssetsForCore(std::vector<ra::data::models::AssetModelBase*>& vAssets, bool bCoreOnly)
@@ -1143,7 +1196,15 @@ bool AssetListViewModel::ValidateAssetsForCore(std::vector<ra::data::models::Ass
     {
         const auto* pAchievement = dynamic_cast<const ra::data::models::AchievementModel*>(pAsset);
         if (pAchievement != nullptr)
+        {
             ValidateAchievementForCore(sError, *pAchievement);
+        }
+        else
+        {
+            const auto* pLeaderboard = dynamic_cast<const ra::data::models::LeaderboardModel*>(pAsset);
+            if (pLeaderboard != nullptr)
+                ValidateLeaderboardForCore(sError, *pLeaderboard);
+        }
     }
 
     if (!sError.empty())
@@ -1430,23 +1491,47 @@ void AssetListViewModel::CreateNew()
     if (!pEmulatorContext.WarnDisableHardcoreMode("create a new asset"))
         return;
 
-    RA_LOG_INFO("Creating new achievement");
+    auto nType = GetAssetTypeFilter();
+    if (nType == ra::data::models::AssetType::None)
+    {
+        ra::ui::viewmodels::NewAssetViewModel vmNewAsset;
+        if (vmNewAsset.ShowModal(*this) != DialogResult::OK)
+            return;
 
-    auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
-    const auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
+        nType = vmNewAsset.GetSelectedType();
+    }
 
     FilteredAssets().BeginUpdate();
 
-    auto& vmAchievement = pGameContext.Assets().NewAchievement();
-    vmAchievement.SetCategory(ra::data::models::AssetCategory::Local);
-    vmAchievement.SetPoints(0);
-    vmAchievement.SetAuthor(ra::Widen(pUserContext.GetUsername()));
-    vmAchievement.UpdateServerCheckpoint();
-    vmAchievement.SetNew();
+    auto& pGameContext = ra::services::ServiceLocator::GetMutable<ra::data::context::GameContext>();
+    ra::data::models::AssetModelBase* pNewAsset = nullptr;
+    switch (nType)
+    {
+        case ra::data::models::AssetType::Achievement:
+            RA_LOG_INFO("Creating new achievement");
+            pNewAsset = &pGameContext.Assets().NewAchievement();
+            break;
+
+        case ra::data::models::AssetType::Leaderboard:
+            RA_LOG_INFO("Creating new leaderboard");
+            pNewAsset = &pGameContext.Assets().NewLeaderboard();
+            break;
+
+        default:
+            break;
+    }
+
+    Expects(pNewAsset != nullptr);
+
+    const auto& pUserContext = ra::services::ServiceLocator::GetMutable<ra::data::context::UserContext>();
+    pNewAsset->SetAuthor(ra::Widen(pUserContext.GetUsername()));
+    pNewAsset->SetCategory(ra::data::models::AssetCategory::Local);
+    pNewAsset->UpdateServerCheckpoint();
+    pNewAsset->SetNew();
 
-    EnsureAppearsInFilteredList(vmAchievement);
+    EnsureAppearsInFilteredList(*pNewAsset);
 
-    const auto nId = ra::to_signed(vmAchievement.GetID());
+    const auto nId = ra::to_signed(pNewAsset->GetID());
 
     // select the new viewmodel, and deselect everything else
     gsl::index nIndex = -1;
@@ -1455,7 +1540,7 @@ void AssetListViewModel::CreateNew()
         auto* pItem = m_vFilteredAssets.GetItemAt(i);
         if (pItem != nullptr)
         {
-            if (pItem->GetId() == nId && pItem->GetType() == ra::data::models::AssetType::Achievement)
+            if (pItem->GetId() == nId && pItem->GetType() == nType)
             {
                 pItem->SetSelected(true);
                 nIndex = i;
diff --git a/src/ui/viewmodels/AssetListViewModel.hh b/src/ui/viewmodels/AssetListViewModel.hh
index ff522fd2..734b9e2b 100644
--- a/src/ui/viewmodels/AssetListViewModel.hh
+++ b/src/ui/viewmodels/AssetListViewModel.hh
@@ -270,6 +270,7 @@ protected:
     virtual void Publish(std::vector<ra::data::models::AssetModelBase*>& vAssets);
     bool ValidateAssetsForCore(std::vector<ra::data::models::AssetModelBase*>& vAssets, bool bCoreOnly);
     virtual void ValidateAchievementForCore(_UNUSED std::wstring& sError, _UNUSED const ra::data::models::AchievementModel& pAchievement) const noexcept(false);
+    virtual void ValidateLeaderboardForCore(_UNUSED std::wstring& sError, _UNUSED const ra::data::models::LeaderboardModel& pAchievement) const noexcept(false);
     virtual bool SelectionContainsInvalidAsset(const std::vector<ra::data::models::AssetModelBase*>& vSelectedAssets, _Out_ std::wstring& sErrorMessage) const;
 
 private:
diff --git a/src/ui/viewmodels/MemorySearchViewModel.cpp b/src/ui/viewmodels/MemorySearchViewModel.cpp
index e94937c4..24a81740 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.cpp
+++ b/src/ui/viewmodels/MemorySearchViewModel.cpp
@@ -99,6 +99,8 @@ MemorySearchViewModel::MemorySearchViewModel()
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::ThirtyTwoBitAligned), L"32-bit (aligned)");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::SixteenBitBigEndian), L"16-bit BE");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::ThirtyTwoBitBigEndian), L"32-bit BE");
+    m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::Float), L"Float");
+    m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::MBF32), L"MBF32");
     m_vSearchTypes.Add(ra::etoi(ra::services::SearchType::AsciiText), L"ASCII Text");
 
     m_vComparisonTypes.Add(ra::etoi(ComparisonType::Equals), L"=");
@@ -352,7 +354,7 @@ void MemorySearchViewModel::DoFrame()
         pCurrentResults.pResults.GetMatchingAddress(nIndex++, pResult);
 
         const auto nPreviousValue = pResult.nValue;
-        UpdateResult(*pRow, pCurrentResults.pResults, pResult, pEmulatorContext);
+        UpdateResult(*pRow, pCurrentResults.pResults, pResult, false, pEmulatorContext);
 
         // when updating an existing result, check to see if it's been modified and update the tracker
         if (!pRow->bHasBeenModified && pResult.nValue != nPreviousValue)
@@ -501,14 +503,14 @@ void MemorySearchViewModel::BeginNewSearch()
     SetValue(ResultMemSizeProperty, ra::etoi(pResult.pResults.GetSize()));
 }
 
-void MemorySearchViewModel::AddNewPage()
+void MemorySearchViewModel::AddNewPage(SearchResult&& pNewPage)
 {
     // remove any search history after the current node
     while (m_vSearchResults.size() - 1 > m_nSelectedSearchResult)
         m_vSearchResults.pop_back();
 
     // add a new search history entry
-    m_vSearchResults.emplace_back();
+    m_vSearchResults.emplace_back(std::move(pNewPage));
     if (m_vSearchResults.size() > SEARCH_MAX_HISTORY + 1)
     {
         // always discard the second search result in case the user wants to do an initial search later
@@ -525,51 +527,18 @@ void MemorySearchViewModel::ApplyFilter()
     if (m_vSearchResults.empty())
         return;
 
-    unsigned int nValue = 0U;
-    if (GetValue(CanEditFilterValueProperty))
-    {
-        const auto& sValue = GetFilterValue();
-        if (sValue.empty())
-        {
-            ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Invalid filter value");
-            return;
-        }
+    const std::wstring sEmptyString;
+    const auto* sValue = GetValue(CanEditFilterValueProperty) ? &GetFilterValue() : &sEmptyString;
 
-        switch (GetSearchType())
-        {
-            case ra::services::SearchType::AsciiText:
-                break;
+    SearchResult& pPreviousResult = m_vSearchResults.back();
+    SearchResult pResult;
 
-            default:
-            {
-                const wchar_t* pStart = sValue.c_str();
-
-                // try decimal first
-                wchar_t* pEnd;
-                nValue = std::wcstoul(pStart, &pEnd, 10);
-                if (pEnd && *pEnd)
-                {
-                    // decimal parse failed, try hex
-                    nValue = std::wcstoul(pStart, &pEnd, 16);
-                    if (*pEnd)
-                    {
-                        // hex parse failed
-                        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Invalid filter value");
-                        return;
-                    }
-                }
-            }
-            break;
-        }
+    if (!ApplyFilter(pResult, pPreviousResult, GetComparisonType(), GetValueType(), *sValue))
+    {
+        ra::ui::viewmodels::MessageBoxViewModel::ShowErrorMessage(L"Invalid filter value");
+        return;
     }
 
-    AddNewPage();
-
-    SearchResult const& pPreviousResult = *(m_vSearchResults.end() - 2);
-    SearchResult& pResult = m_vSearchResults.back();
-
-    ApplyFilter(pResult, pPreviousResult, GetComparisonType(), GetValueType(), nValue, GetFilterValue());
-
     // if this isn't the first filter being applied, and the result count hasn't changed
     const auto nMatches = pResult.pResults.MatchingAddressCount();
     if (nMatches == pPreviousResult.pResults.MatchingAddressCount() && m_vSearchResults.size() > 2)
@@ -583,9 +552,7 @@ void MemorySearchViewModel::ApplyFilter()
             // didn't change, so don't generate a new result set. do clear the modified addresses list.
             if (pResult.pResults.GetFilterComparison() == ComparisonType::Equals)
             {
-                m_vSearchResults.pop_back();
-                m_vSearchResults.back().vModifiedAddresses.clear();
-                --m_nSelectedSearchResult;
+                pPreviousResult.vModifiedAddresses.clear();
                 return;
             }
         }
@@ -621,41 +588,22 @@ void MemorySearchViewModel::ApplyFilter()
     pResult.sSummary = builder.ToWString();
     SetValue(FilterSummaryProperty, pResult.sSummary);
 
+    AddNewPage(std::move(pResult));
     ChangePage(m_nSelectedSearchResult);
 }
 
-void MemorySearchViewModel::ApplyFilter(SearchResult& pResult, const SearchResult& pPreviousResult,
-    ComparisonType nComparisonType, ra::services::SearchFilterType nValueType, unsigned int nValue, const std::wstring& sValue)
+bool MemorySearchViewModel::ApplyFilter(SearchResult& pResult, const SearchResult& pPreviousResult,
+    ComparisonType nComparisonType, ra::services::SearchFilterType nValueType, const std::wstring& sValue)
 {
     if (nValueType == ra::services::SearchFilterType::InitialValue)
     {
         SearchResult const& pInitialResult = m_vSearchResults.front();
-
-        switch (pPreviousResult.pResults.GetSearchType())
-        {
-            case ra::services::SearchType::AsciiText:
-                pResult.pResults.Initialize(pInitialResult.pResults, pPreviousResult.pResults,
-                    nComparisonType, nValueType, sValue);
-                break;
-
-            default:
-                pResult.pResults.Initialize(pInitialResult.pResults, pPreviousResult.pResults,
-                    nComparisonType, nValueType, nValue);
-                break;
-        }
+        return pResult.pResults.Initialize(pInitialResult.pResults, pPreviousResult.pResults,
+            nComparisonType, nValueType, sValue);
     }
     else
     {
-        switch (pPreviousResult.pResults.GetSearchType())
-        {
-            case ra::services::SearchType::AsciiText:
-                pResult.pResults.Initialize(pPreviousResult.pResults, nComparisonType, nValueType, sValue);
-                break;
-
-            default:
-                pResult.pResults.Initialize(pPreviousResult.pResults, nComparisonType, nValueType, nValue);
-                break;
-        }
+        return pResult.pResults.Initialize(pPreviousResult.pResults, nComparisonType, nValueType, sValue);
     }
 }
 
@@ -704,8 +652,7 @@ void MemorySearchViewModel::ApplyContinuousFilter()
     // apply the current filter
     SearchResult pNewResult;
     ApplyFilter(pNewResult, pResult, pResult.pResults.GetFilterComparison(),
-        pResult.pResults.GetFilterType(), pResult.pResults.GetFilterValue(),
-        pResult.pResults.GetFilterString());
+        pResult.pResults.GetFilterType(), pResult.pResults.GetFilterString());
     pNewResult.sSummary = pResult.sSummary;
     const auto nNewResults = pNewResult.pResults.MatchingAddressCount();
 
@@ -792,7 +739,7 @@ void MemorySearchViewModel::UpdateResults()
 
         pRow->SetAddress(ra::Widen(sAddress));
 
-        UpdateResult(*pRow, pCurrentResults.pResults, pResult, pEmulatorContext);
+        UpdateResult(*pRow, pCurrentResults.pResults, pResult, true, pEmulatorContext);
 
         const auto pCodeNote = pGameContext.FindCodeNote(pResult.nAddress, pResult.nSize);
         if (!pCodeNote.empty())
@@ -834,27 +781,18 @@ void MemorySearchViewModel::UpdateResults()
 }
 
 void MemorySearchViewModel::UpdateResult(SearchResultViewModel& vmResult,
-    const ra::services::SearchResults& pResults,
-    ra::services::SearchResults::Result& pResult,
-    const ra::data::context::EmulatorContext& pEmulatorContext)
+    const ra::services::SearchResults& pResults, ra::services::SearchResults::Result& pResult,
+    bool bForceFilterCheck, const ra::data::context::EmulatorContext& pEmulatorContext)
 {
-    const auto nPreviousValue = pResult.nValue;
     std::wstring sFormattedValue;
-
-    pResults.UpdateValue(pResult, &sFormattedValue, pEmulatorContext);
-    vmResult.SetCurrentValue(sFormattedValue);
-
-    switch (pResults.GetSearchType())
+    if (pResults.UpdateValue(pResult, &sFormattedValue, pEmulatorContext) || bForceFilterCheck)
     {
-        case ra::services::SearchType::AsciiText:
-            // TODO: implement for strings
-            vmResult.bMatchesFilter = true;
-            break;
+        if (pResults.GetFilterType() == ra::services::SearchFilterType::InitialValue)
+            vmResult.bMatchesFilter = pResults.MatchesFilter(m_vSearchResults.front().pResults, pResult);
+        else
+            vmResult.bMatchesFilter = pResults.MatchesFilter(m_vSearchResults.at(m_nSelectedSearchResult - 1).pResults, pResult);
 
-        default:
-            // when continuous filtering, the values should always match - don't bother testing
-            vmResult.bMatchesFilter = m_bIsContinuousFiltering || TestFilter(pResult, pResults, nPreviousValue);
-            break;
+        vmResult.SetCurrentValue(sFormattedValue);
     }
 }
 
@@ -897,29 +835,6 @@ void MemorySearchViewModel::OnCodeNoteChanged(ra::ByteAddress nAddress, const st
     }
 }
 
-bool MemorySearchViewModel::TestFilter(const ra::services::SearchResults::Result& pResult,
-    const ra::services::SearchResults& pResults, unsigned int nPreviousValue) noexcept
-{
-    switch (pResults.GetFilterType())
-    {
-        case ra::services::SearchFilterType::Constant:
-            return pResult.Compare(pResults.GetFilterValue(), pResults.GetFilterComparison());
-
-        case ra::services::SearchFilterType::InitialValue:
-        case ra::services::SearchFilterType::LastKnownValue:
-            return pResult.Compare(nPreviousValue, pResults.GetFilterComparison());
-
-        case ra::services::SearchFilterType::LastKnownValuePlus:
-            return pResult.Compare(nPreviousValue + pResults.GetFilterValue(), pResults.GetFilterComparison());
-
-        case ra::services::SearchFilterType::LastKnownValueMinus:
-            return pResult.Compare(nPreviousValue - pResults.GetFilterValue(), pResults.GetFilterComparison());
-
-        default:
-            return false;
-    }
-}
-
 static constexpr bool CanEditFilterValue(ra::services::SearchFilterType nFilterType)
 {
     switch (nFilterType)
@@ -1091,12 +1006,8 @@ void MemorySearchViewModel::ExcludeSelected()
     if (m_vSelectedAddresses.empty())
         return;
 
-    AddNewPage();
-
-    SearchResult pPreviousResult = *(m_vSearchResults.end() - 2);
-
-    m_vSearchResults.pop_back(); // remove temporary item
-    SearchResult& pResult = m_vSearchResults.emplace_back(pPreviousResult); // clone previous item
+    const SearchResult& pPreviousResult = m_vSearchResults.back();
+    SearchResult pResult{ pPreviousResult }; // clone previous item
 
     for (const auto nAddress : m_vSelectedAddresses)
         pResult.pResults.ExcludeAddress(nAddress);
@@ -1116,9 +1027,11 @@ void MemorySearchViewModel::ExcludeSelected()
     m_vSelectedAddresses.clear();
     SetValue(HasSelectionProperty, false);
 
+    const size_t nMatchingAddressCount = pResult.pResults.MatchingAddressCount();
+    AddNewPage(std::move(pResult));
     ChangePage(m_nSelectedSearchResult);
 
-    if (pResult.pResults.MatchingAddressCount() < gsl::narrow_cast<size_t>(nScrollOffset) + SEARCH_ROWS_DISPLAYED)
+    if (nMatchingAddressCount < gsl::narrow_cast<size_t>(nScrollOffset) + SEARCH_ROWS_DISPLAYED)
         nScrollOffset = 0;
 
     SetValue(ScrollOffsetProperty, nScrollOffset);
diff --git a/src/ui/viewmodels/MemorySearchViewModel.hh b/src/ui/viewmodels/MemorySearchViewModel.hh
index 98d095a3..0f9a6c12 100644
--- a/src/ui/viewmodels/MemorySearchViewModel.hh
+++ b/src/ui/viewmodels/MemorySearchViewModel.hh
@@ -459,12 +459,9 @@ private:
     void ApplyContinuousFilter();
     void UpdateResults();
     void UpdateResult(SearchResultViewModel& pRow, const ra::services::SearchResults& pResults,
-        ra::services::SearchResults::Result& pResult,
+        ra::services::SearchResults::Result& pResult, bool bForceFilterCheck,
         const ra::data::context::EmulatorContext& pEmulatorContext);
 
-    void AddNewPage();
-    void ChangePage(size_t nNewPage);
-
     void OnPredefinedFilterRangeChanged();
     void OnFilterRangeChanged();
 
@@ -487,13 +484,15 @@ private:
         std::wstring sSummary;
     };
 
+    void AddNewPage(SearchResult&& pNewPage);
+    void ChangePage(size_t nNewPage);
+
     size_t m_nSelectedSearchResult = 0U;
     std::vector<SearchResult> m_vSearchResults;
     std::set<unsigned int> m_vSelectedAddresses;
 
-    static bool TestFilter(const ra::services::SearchResults::Result& pResult, const ra::services::SearchResults& pResults, unsigned int nPreviousValue) noexcept;
-    void ApplyFilter(SearchResult& pResult, const SearchResult& pPreviousResult,
-        ComparisonType nComparisonType, ra::services::SearchFilterType nValueType, unsigned int nValue, const std::wstring& sValue);
+    bool ApplyFilter(SearchResult& pResult, const SearchResult& pPreviousResult,
+        ComparisonType nComparisonType, ra::services::SearchFilterType nValueType, const std::wstring& sValue);
 };
 
 } // namespace viewmodels
diff --git a/src/ui/viewmodels/NewAssetViewModel.cpp b/src/ui/viewmodels/NewAssetViewModel.cpp
new file mode 100644
index 00000000..fe78d3f1
--- /dev/null
+++ b/src/ui/viewmodels/NewAssetViewModel.cpp
@@ -0,0 +1,35 @@
+#include "NewAssetViewModel.hh"
+
+#include "RA_StringUtils.h"
+
+#include "data\context\GameContext.hh"
+
+#include "services\IClipboard.hh"
+#include "services\ServiceLocator.hh"
+
+namespace ra {
+namespace ui {
+namespace viewmodels {
+
+const IntModelProperty NewAssetViewModel::AssetTypeProperty("NewAssetViewModel", "AssetType", ra::etoi(ra::data::models::AssetType::Achievement));
+
+NewAssetViewModel::NewAssetViewModel() noexcept
+{
+    SetWindowTitle(L"New Asset");
+}
+
+void NewAssetViewModel::NewAchievement()
+{
+    SetValue(AssetTypeProperty, ra::etoi(ra::data::models::AssetType::Achievement));
+    SetDialogResult(DialogResult::OK);
+}
+
+void NewAssetViewModel::NewLeaderboard()
+{
+    SetValue(AssetTypeProperty, ra::etoi(ra::data::models::AssetType::Leaderboard));
+    SetDialogResult(DialogResult::OK);
+}
+
+} // namespace viewmodels
+} // namespace ui
+} // namespace ra
diff --git a/src/ui/viewmodels/NewAssetViewModel.hh b/src/ui/viewmodels/NewAssetViewModel.hh
new file mode 100644
index 00000000..46a62004
--- /dev/null
+++ b/src/ui/viewmodels/NewAssetViewModel.hh
@@ -0,0 +1,49 @@
+#ifndef RA_UI_NEWASSETVIEWMODEL_H
+#define RA_UI_NEWASSETVIEWMODEL_H
+#pragma once
+
+#include "data/models/AssetModelBase.hh"
+
+#include "ui/WindowViewModelBase.hh"
+
+namespace ra {
+namespace ui {
+namespace viewmodels {
+
+class NewAssetViewModel : public WindowViewModelBase
+{
+public:
+    GSL_SUPPRESS_F6 NewAssetViewModel() noexcept;
+    ~NewAssetViewModel() = default;
+
+    NewAssetViewModel(const NewAssetViewModel&) noexcept = delete;
+    NewAssetViewModel& operator=(const NewAssetViewModel&) noexcept = delete;
+    NewAssetViewModel(NewAssetViewModel&&) noexcept = delete;
+    NewAssetViewModel& operator=(NewAssetViewModel&&) noexcept = delete;
+
+    /// <summary>
+    /// Selects New Achievement and closes the dialog.
+    /// </summary>
+    void NewAchievement();
+
+    /// <summary>
+    /// Selects New Leaderboard and closes the dialog.
+    /// </summary>
+    void NewLeaderboard();
+
+    /// <summary>
+    /// The <see cref="ModelProperty" /> for the selected asset type.
+    /// </summary>
+    static const IntModelProperty AssetTypeProperty;
+
+    /// <summary>
+    /// Gets the asset category.
+    /// </summary>
+    ra::data::models::AssetType GetSelectedType() const { return ra::itoe<ra::data::models::AssetType>(GetValue(AssetTypeProperty)); }
+};
+
+} // namespace viewmodels
+} // namespace ui
+} // namespace ra
+
+#endif !RA_UI_NEWASSETVIEWMODEL_H
diff --git a/src/ui/win32/AssetEditorDialog.cpp b/src/ui/win32/AssetEditorDialog.cpp
index 68e1e100..a64d64b1 100644
--- a/src/ui/win32/AssetEditorDialog.cpp
+++ b/src/ui/win32/AssetEditorDialog.cpp
@@ -868,6 +868,7 @@ BOOL AssetEditorDialog::OnInitDialog()
 
     m_bindErrorIcon.SetControl(*this, IDC_RA_ERROR_INDICATOR);
     m_bindMeasuredAsPercent.SetControl(*this, IDC_RA_CHK_AS_PERCENT);
+    m_bindLowerIsBetter.SetControl(*this, IDC_RA_CHK_LOWER_IS_BETTER);
     m_bindDebugHighlights.SetControl(*this, IDC_RA_CHK_HIGHLIGHTS);
     m_bindPauseOnReset.SetControl(*this, IDC_RA_CHK_PAUSE_ON_RESET);
     m_bindPauseOnTrigger.SetControl(*this, IDC_RA_CHK_PAUSE_ON_TRIGGER);
diff --git a/src/ui/win32/AssetListDialog.cpp b/src/ui/win32/AssetListDialog.cpp
index 4d29280f..de0a4ea2 100644
--- a/src/ui/win32/AssetListDialog.cpp
+++ b/src/ui/win32/AssetListDialog.cpp
@@ -246,7 +246,7 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     m_bindAssets.BindItems(vmAssetList.FilteredAssets());
     m_bindAssets.BindIsSelected(AssetListViewModel::AssetSummaryViewModel::IsSelectedProperty);
 
-    m_bindWindow.BindLabel(IDC_RA_GAMEHASH, AssetListViewModel::GameIdProperty);
+    m_bindWindow.BindLabel(IDC_RA_ID, AssetListViewModel::GameIdProperty);
     m_bindWindow.BindLabel(IDC_RA_NUMACH, AssetListViewModel::AchievementCountProperty);
     m_bindWindow.BindLabel(IDC_RA_POINT_TOTAL, AssetListViewModel::TotalPointsProperty);
 
@@ -258,8 +258,8 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     m_bindWindow.BindEnabled(IDC_RA_DOWNLOAD_ACH, AssetListViewModel::CanResetProperty);
     m_bindWindow.BindLabel(IDC_RA_REVERTSELECTED, AssetListViewModel::RevertButtonTextProperty);
     m_bindWindow.BindEnabled(IDC_RA_REVERTSELECTED, AssetListViewModel::CanRevertProperty);
-    m_bindWindow.BindEnabled(IDC_RA_ADD_ACH, AssetListViewModel::CanCreateProperty);
-    m_bindWindow.BindEnabled(IDC_RA_CLONE_ACH, AssetListViewModel::CanCloneProperty);
+    m_bindWindow.BindEnabled(IDC_RA_NEW_ASSET, AssetListViewModel::CanCreateProperty);
+    m_bindWindow.BindEnabled(IDC_RA_CLONE_ASSET, AssetListViewModel::CanCloneProperty);
 
     m_bindProcessingActive.BindCheck(AssetListViewModel::IsProcessingActiveProperty);
     m_bindKeepActive.BindCheck(AssetListViewModel::KeepActiveProperty);
@@ -267,8 +267,8 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     using namespace ra::bitwise_ops;
     SetAnchor(IDC_RA_CATEGORY, Anchor::Top | Anchor::Left);
     SetAnchor(IDC_RA_SPECIAL_FILTER, Anchor::Top | Anchor::Left);
-    SetAnchor(IDC_RA_GAMEID, Anchor::Top | Anchor::Right);
-    SetAnchor(IDC_RA_GAMEHASH, Anchor::Top | Anchor::Right);
+    SetAnchor(IDC_RA_LBL_ID, Anchor::Top | Anchor::Right);
+    SetAnchor(IDC_RA_ID, Anchor::Top | Anchor::Right);
     SetAnchor(IDC_RA_ACHIEVEMENTS, Anchor::Top | Anchor::Right);
     SetAnchor(IDC_RA_NUMACH, Anchor::Top | Anchor::Right);
     SetAnchor(IDC_RA_POINTS, Anchor::Top | Anchor::Right);
@@ -280,8 +280,8 @@ AssetListDialog::AssetListDialog(AssetListViewModel& vmAssetList)
     SetAnchor(IDC_RA_COMMIT_ACH, Anchor::Top | Anchor::Right);
     SetAnchor(IDC_RA_DOWNLOAD_ACH, Anchor::Top | Anchor::Right);
     SetAnchor(IDC_RA_REVERTSELECTED, Anchor::Top | Anchor::Right);
-    SetAnchor(IDC_RA_ADD_ACH, Anchor::Bottom | Anchor::Right);
-    SetAnchor(IDC_RA_CLONE_ACH, Anchor::Bottom | Anchor::Right);
+    SetAnchor(IDC_RA_NEW_ASSET, Anchor::Bottom | Anchor::Right);
+    SetAnchor(IDC_RA_CLONE_ASSET, Anchor::Bottom | Anchor::Right);
 
     SetMinimumSize(640, 293);
 }
@@ -341,7 +341,7 @@ BOOL AssetListDialog::OnCommand(WORD nCommand)
             return TRUE;
         }
 
-        case IDC_RA_ADD_ACH:
+        case IDC_RA_NEW_ASSET:
         {
             auto* vmAssets = dynamic_cast<AssetListViewModel*>(&m_vmWindow);
             if (vmAssets)
@@ -350,7 +350,7 @@ BOOL AssetListDialog::OnCommand(WORD nCommand)
             return TRUE;
         }
 
-        case IDC_RA_CLONE_ACH:
+        case IDC_RA_CLONE_ASSET:
         {
             auto* vmAssets = dynamic_cast<AssetListViewModel*>(&m_vmWindow);
             if (vmAssets)
diff --git a/src/ui/win32/Desktop.cpp b/src/ui/win32/Desktop.cpp
index 7211395d..b07892c7 100644
--- a/src/ui/win32/Desktop.cpp
+++ b/src/ui/win32/Desktop.cpp
@@ -16,6 +16,7 @@
 #include "ui/win32/MessageBoxDialog.hh"
 #include "ui/win32/MemoryBookmarksDialog.hh"
 #include "ui/win32/MemoryInspectorDialog.hh"
+#include "ui/win32/NewAssetDialog.hh"
 #include "ui/win32/OverlaySettingsDialog.hh"
 #include "ui/win32/ProgressDialog.hh"
 #include "ui/win32/RichPresenceDialog.hh"
@@ -45,6 +46,7 @@ Desktop::Desktop() noexcept
     m_vDialogPresenters.emplace_back(new (std::nothrow) AssetEditorDialog::Presenter);
     m_vDialogPresenters.emplace_back(new (std::nothrow) FileDialog::Presenter);
     m_vDialogPresenters.emplace_back(new (std::nothrow) OverlaySettingsDialog::Presenter);
+    m_vDialogPresenters.emplace_back(new (std::nothrow) NewAssetDialog::Presenter);
     m_vDialogPresenters.emplace_back(new (std::nothrow) BrokenAchievementsDialog::Presenter);
     m_vDialogPresenters.emplace_back(new (std::nothrow) UnknownGameDialog::Presenter);
     m_vDialogPresenters.emplace_back(new (std::nothrow) ProgressDialog::Presenter);
diff --git a/src/ui/win32/NewAssetDialog.cpp b/src/ui/win32/NewAssetDialog.cpp
new file mode 100644
index 00000000..fa1eff76
--- /dev/null
+++ b/src/ui/win32/NewAssetDialog.cpp
@@ -0,0 +1,59 @@
+#include "NewAssetDialog.hh"
+
+#include "RA_Core.h"
+#include "RA_Resource.h"
+
+namespace ra {
+namespace ui {
+namespace win32 {
+
+bool NewAssetDialog::Presenter::IsSupported(const ra::ui::WindowViewModelBase& vmViewModel) noexcept
+{
+    return (dynamic_cast<const ra::ui::viewmodels::NewAssetViewModel*>(&vmViewModel) != nullptr);
+}
+
+void NewAssetDialog::Presenter::ShowModal(ra::ui::WindowViewModelBase& vmViewModel, HWND hParentWnd)
+{
+    auto* vmGameChecksum = dynamic_cast<ra::ui::viewmodels::NewAssetViewModel*>(&vmViewModel);
+    Expects(vmGameChecksum != nullptr);
+
+    NewAssetDialog oDialog(*vmGameChecksum);
+    oDialog.CreateModalWindow(MAKEINTRESOURCE(IDD_RA_NEWASSET), this, hParentWnd);
+}
+
+void NewAssetDialog::Presenter::ShowWindow(ra::ui::WindowViewModelBase& oViewModel)
+{
+    ShowModal(oViewModel, nullptr);
+}
+
+// ------------------------------------
+
+NewAssetDialog::NewAssetDialog(ra::ui::viewmodels::NewAssetViewModel& vmGameChecksum) noexcept
+    : DialogBase(vmGameChecksum)
+{
+}
+
+BOOL NewAssetDialog::OnCommand(WORD nCommand)
+{
+    if (nCommand == IDC_RA_NEW_ACHIEVEMENT)
+    {
+        auto* vmNewAsset = dynamic_cast<ra::ui::viewmodels::NewAssetViewModel*>(&m_vmWindow);
+        if (vmNewAsset)
+            vmNewAsset->NewAchievement();
+        return TRUE;
+    }
+
+    if (nCommand == IDC_RA_NEW_LEADERBOARD)
+    {
+        auto* vmNewAsset = dynamic_cast<ra::ui::viewmodels::NewAssetViewModel*>(&m_vmWindow);
+        if (vmNewAsset)
+            vmNewAsset->NewLeaderboard();
+        return TRUE;
+    }
+
+    return DialogBase::OnCommand(nCommand);
+}
+
+} // namespace win32
+} // namespace ui
+} // namespace ra
diff --git a/src/ui/win32/NewAssetDialog.hh b/src/ui/win32/NewAssetDialog.hh
new file mode 100644
index 00000000..c239810a
--- /dev/null
+++ b/src/ui/win32/NewAssetDialog.hh
@@ -0,0 +1,41 @@
+#ifndef RA_UI_WIN32_DLG_NEWASSET_H
+#define RA_UI_WIN32_DLG_NEWASSET_H
+#pragma once
+
+#include "ui/viewmodels/NewAssetViewModel.hh"
+#include "ui/win32/DialogBase.hh"
+#include "ui/win32/IDialogPresenter.hh"
+
+#include <memory>
+
+namespace ra {
+namespace ui {
+namespace win32 {
+
+class NewAssetDialog : public DialogBase
+{
+public:
+    explicit NewAssetDialog(ra::ui::viewmodels::NewAssetViewModel& vmNewAsset) noexcept;
+    virtual ~NewAssetDialog() noexcept = default;
+    NewAssetDialog(const NewAssetDialog&) noexcept = delete;
+    NewAssetDialog& operator=(const NewAssetDialog&) noexcept = delete;
+    NewAssetDialog(NewAssetDialog&&) noexcept = delete;
+    NewAssetDialog& operator=(NewAssetDialog&&) noexcept = delete;
+
+    class Presenter : public IDialogPresenter
+    {
+    public:
+        bool IsSupported(const ra::ui::WindowViewModelBase& viewModel) noexcept override;
+        void ShowWindow(ra::ui::WindowViewModelBase& viewModel) override;
+        void ShowModal(ra::ui::WindowViewModelBase& viewModel, HWND hParentWnd) override;
+    };
+
+protected:
+    BOOL OnCommand(WORD nCommand) override;
+};
+
+} // namespace win32
+} // namespace ui
+} // namespace ra
+
+#endif // !RA_UI_WIN32_DLG_NEWASSET_H


Test patch:
diff --git a/tests/RA_Integration.Tests.vcxproj b/tests/RA_Integration.Tests.vcxproj
index 16ca9746..a05fae75 100644
--- a/tests/RA_Integration.Tests.vcxproj
+++ b/tests/RA_Integration.Tests.vcxproj
@@ -337,6 +337,7 @@
     <ClCompile Include="..\src\ui\viewmodels\MemoryInspectorViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\MemorySearchViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\MemoryViewerViewModel.cpp" />
+    <ClCompile Include="..\src\ui\viewmodels\NewAssetViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\OverlayAchievementsPageViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\OverlayFriendsPageViewModel.cpp" />
     <ClCompile Include="..\src\ui\viewmodels\OverlayLeaderboardsPageViewModel.cpp" />
diff --git a/tests/RA_Integration.Tests.vcxproj.filters b/tests/RA_Integration.Tests.vcxproj.filters
index 0a07fa1f..75c7b347 100644
--- a/tests/RA_Integration.Tests.vcxproj.filters
+++ b/tests/RA_Integration.Tests.vcxproj.filters
@@ -429,6 +429,9 @@
     <ClCompile Include="data\models\TriggerValidation_Tests.cpp">
       <Filter>Tests\Data\Models</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\ui\viewmodels\NewAssetViewModel.cpp">
+      <Filter>Code</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <None Include="base.props" />
diff --git a/tests/data/context/GameContext_Tests.cpp b/tests/data/context/GameContext_Tests.cpp
index d7cb85f5..af70f8ef 100644
--- a/tests/data/context/GameContext_Tests.cpp
+++ b/tests/data/context/GameContext_Tests.cpp
@@ -1522,14 +1522,7 @@ public:
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
             response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request&, ra::api::FetchUserUnlocks::Response& response)
-        {
-            response.UnlockedAchievements.insert(1U);
-            response.UnlockedAchievements.insert(2U);
-            response.Result = ra::api::ApiResult::Success;
+            response.AchievementsRemaining = 0;
             return true;
         });
 
@@ -1564,7 +1557,6 @@ public:
 
         game.mockThreadPool.ExecuteNextTask(); // award achievement 1
         game.mockThreadPool.ExecuteNextTask(); // award achievement 2
-        game.mockThreadPool.AdvanceTime(std::chrono::milliseconds(500)); // mastery unlock check is delayed
         game.mockThreadPool.ExecuteNextTask(); // check unlocks
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav"));
@@ -1574,6 +1566,9 @@ public:
         Assert::AreEqual(std::wstring(L"Mastered GameName"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"2 achievements, 10 points"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"Username | Play time: 1h18m"), pPopup->GetDetail());
+
+        pPopup = game.mockOverlayManager.GetMessage(4); // mastery should only have been shown once
+        Expects(pPopup == nullptr);
     }
 
     TEST_METHOD(TestAwardAchievementMasteryNonHardcore)
@@ -1588,14 +1583,7 @@ public:
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
             response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request&, ra::api::FetchUserUnlocks::Response& response)
-        {
-            response.UnlockedAchievements.insert(1U);
-            response.UnlockedAchievements.insert(2U);
-            response.Result = ra::api::ApiResult::Success;
+            response.AchievementsRemaining = 0;
             return true;
         });
 
@@ -1630,7 +1618,6 @@ public:
 
         game.mockThreadPool.ExecuteNextTask(); // award achievement 1
         game.mockThreadPool.ExecuteNextTask(); // award achievement 2
-        game.mockThreadPool.AdvanceTime(std::chrono::milliseconds(500)); // mastery unlock check is delayed
         game.mockThreadPool.ExecuteNextTask(); // check unlocks
 
         Assert::IsTrue(game.mockAudioSystem.WasAudioFilePlayed(L"Overlay\\unlock.wav"));
@@ -1640,6 +1627,9 @@ public:
         Assert::AreEqual(std::wstring(L"Completed GameName"), pPopup->GetTitle());
         Assert::AreEqual(std::wstring(L"2 achievements, 10 points"), pPopup->GetDescription());
         Assert::AreEqual(std::wstring(L"Player | Play time: 102h03m"), pPopup->GetDetail());
+
+        pPopup = game.mockOverlayManager.GetMessage(4); // mastery should only have been shown once
+        Expects(pPopup == nullptr);
     }
 
     TEST_METHOD(TestAwardAchievementMasteryIncomplete)
@@ -1653,13 +1643,7 @@ public:
         game.mockServer.HandleRequest<ra::api::AwardAchievement>([](const ra::api::AwardAchievement::Request&, ra::api::AwardAchievement::Response& response)
         {
             response.Result = ra::api::ApiResult::Success;
-            return true;
-        });
-
-        game.mockServer.HandleRequest<ra::api::FetchUserUnlocks>([](const ra::api::FetchUserUnlocks::Request&, ra::api::FetchUserUnlocks::Response& response)
-        {
-            response.UnlockedAchievements.insert(2U);
-            response.Result = ra::api::ApiResult::Success;
+            response.AchievementsRemaining = 1;
             return true;
         });
 
diff --git a/tests/data/models/TriggerValidation_Tests.cpp b/tests/data/models/TriggerValidation_Tests.cpp
index fa6fc1c3..d799680a 100644
--- a/tests/data/models/TriggerValidation_Tests.cpp
+++ b/tests/data/models/TriggerValidation_Tests.cpp
@@ -17,7 +17,15 @@ private:
     void AssertValidation(const std::string& sTrigger, const std::wstring& sExpectedError)
     {
         std::wstring sError;
-        const bool bResult = TriggerValidation::Validate(sTrigger, sError);
+        const bool bResult = TriggerValidation::Validate(sTrigger, sError, AssetType::Achievement);
+        Assert::AreEqual(sExpectedError, sError);
+        Assert::AreEqual(bResult, sExpectedError.empty());
+    }
+
+    void AssertLeaderboardValidation(const std::string& sTrigger, const std::wstring& sExpectedError)
+    {
+        std::wstring sError;
+        const bool bResult = TriggerValidation::Validate(sTrigger, sError, AssetType::Leaderboard);
         Assert::AreEqual(sExpectedError, sError);
         Assert::AreEqual(bResult, sExpectedError.empty());
     }
@@ -145,6 +153,24 @@ public:
         AssertValidation("I:0xX1234_0xHFFFFFF00>5", L"");
         AssertValidation("I:0xX1234_0xH1234>5_0xHFFFFFF00>5", L"Condition 3: Address FFFFFF00 out of range (max 1FFFF)");
     }
+
+    TEST_METHOD(TestLeaderboardConditionTypes)
+    {
+        AssertLeaderboardValidation("0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("P:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("R:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("M:0xH1234<99_0x2345<50", L"Measured has no effect in leaderboard triggers");
+        AssertLeaderboardValidation("Q:0xH1234<99_0x2345<50", L"MeasuredIf has no effect in leaderboard triggers");
+        AssertLeaderboardValidation("T:0xH1234<99_0x2345<50", L"Trigger has no effect in leaderboard triggers");
+        AssertLeaderboardValidation("A:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("B:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("I:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("C:0xH1234<99_0x2345<50.6.", L"");
+        AssertLeaderboardValidation("D:0xH1234<99_0x2345<50.6.", L"");
+        AssertLeaderboardValidation("Z:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("N:0xH1234<99_0x2345<50", L"");
+        AssertLeaderboardValidation("O:0xH1234<99_0x2345<50", L"");
+    }
 };
 
 
diff --git a/tests/services/SearchResults_Tests.cpp b/tests/services/SearchResults_Tests.cpp
index 797ca0aa..308ca0ee 100644
--- a/tests/services/SearchResults_Tests.cpp
+++ b/tests/services/SearchResults_Tests.cpp
@@ -314,7 +314,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0xABU);
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"0xAB");
 
         Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
         Assert::IsFalse(results2.ContainsAddress(0U));
@@ -330,6 +330,25 @@ public:
         Assert::AreEqual(0xABU, result.nValue);
     }
 
+    TEST_METHOD(TestInitializeFromResultsEightBitEqualsInvalidConstant)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+        Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
+
+        SearchResults results2;
+        Assert::IsFalse(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L""));
+        Assert::IsFalse(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"Banana"));
+        Assert::IsFalse(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"3.14"));
+        Assert::IsFalse(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"$1234"));
+        Assert::IsTrue(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"ABBA")); // can be read as hex
+        Assert::IsTrue(results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"0xAB"));
+    }
+
     TEST_METHOD(TestInitializeFromResultsEightBitEqualsConstantGap)
     {
         std::array<unsigned char, 6> memory{ 0x00, 0x12, 0x00, 0x00, 0x56, 0x00 };
@@ -341,13 +360,13 @@ public:
         Assert::AreEqual({ 6U }, results1.MatchingAddressCount());
 
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0x00U);
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"0");
         Assert::AreEqual({ 4U }, results2.MatchingAddressCount());
 
         memory.at(1) = 0x00; // not previously matched, should be ignored
         memory.at(3) = 0x01; // previously matched, should no longer match
         SearchResults results3;
-        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0x00U);
+        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"0");
 
         Assert::AreEqual({ 3U }, results3.MatchingAddressCount());
         Assert::IsTrue(results3.ContainsAddress(0U));
@@ -385,7 +404,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::Constant, 0xABU);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::Constant, L"0xAB");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -417,7 +436,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::Constant, 0x34U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::Constant, L"0x34");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -449,7 +468,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::Constant, 0x34U);
+        results.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::Constant, L"0x34");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -476,7 +495,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::GreaterThanOrEqual, ra::services::SearchFilterType::Constant, 0x34U);
+        results.Initialize(results1, ComparisonType::GreaterThanOrEqual, ra::services::SearchFilterType::Constant, L"0x34");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -508,7 +527,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::LessThan, ra::services::SearchFilterType::Constant, 0x34U);
+        results.Initialize(results1, ComparisonType::LessThan, ra::services::SearchFilterType::Constant, L"0x34");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -535,7 +554,7 @@ public:
         Assert::AreEqual({ 3U }, results1.MatchingAddressCount());
 
         SearchResults results;
-        results.Initialize(results1, ComparisonType::LessThanOrEqual, ra::services::SearchFilterType::Constant, 0x34U);
+        results.Initialize(results1, ComparisonType::LessThanOrEqual, ra::services::SearchFilterType::Constant, L"0x34");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -570,7 +589,7 @@ public:
         memory.at(1) = 0xAB;
         memory.at(3) = 0x12;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 0xABU);
+        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"0xAB");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -599,7 +618,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 3U }, results2.MatchingAddressCount());
         Assert::IsTrue(results2.ContainsAddress(0U));
@@ -610,7 +629,7 @@ public:
 
         // no change - tests the "entire block matches" optimization
         SearchResults results3;
-        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results3.Initialize(results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 3U }, results3.MatchingAddressCount());
         Assert::IsTrue(results3.ContainsAddress(0U));
@@ -628,7 +647,7 @@ public:
         // no change - tests the "entire block matches" optimization
         SearchResults results4;
         memory.at(3) = 0x99;
-        results4.Initialize(results3, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results4.Initialize(results3, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results4.MatchingAddressCount());
         Assert::IsTrue(results4.ContainsAddress(0U));
@@ -656,7 +675,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
         Assert::IsFalse(results2.ContainsAddress(0U));
@@ -678,7 +697,7 @@ public:
 
         // no change - tests the "entire block matches" optimization
         SearchResults results3;
-        results3.Initialize(results2, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results3.Initialize(results2, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
         Assert::AreEqual({ 0U }, results3.MatchingAddressCount());
     }
 
@@ -700,7 +719,7 @@ public:
         memory.at(99) = 0x99;
         memory.at(700) = 0x00;
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         // expectation: results2 should allocate several smaller blocks instead of one large one
         Assert::AreEqual({ 5U }, results2.MatchingAddressCount());
@@ -750,7 +769,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x36;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValuePlus, 2U);
+        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValuePlus, L"2");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -784,7 +803,7 @@ public:
         memory.at(1) = 0x10;
         memory.at(2) = 0x32;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValueMinus, 2U);
+        results.Initialize(results1, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValueMinus, L"2");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -818,7 +837,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x37;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::LastKnownValuePlus, 2U);
+        results.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::LastKnownValuePlus, L"2");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -846,7 +865,7 @@ public:
 
         memory.at(2) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -878,7 +897,7 @@ public:
 
         memory.at(4) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -904,7 +923,7 @@ public:
         memory.at(1) = 0x55;
         memory.at(3) = 0x66;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsTrue(results.ContainsAddress(0U));
@@ -923,7 +942,7 @@ public:
 
         memory.at(2) = 0x99;
         SearchResults results2;
-        results2.Initialize(results, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
         Assert::IsFalse(results2.ContainsAddress(0U));
@@ -948,7 +967,7 @@ public:
         memory.at(2) = 0x55;
         memory.at(4) = 0x66;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsTrue(results.ContainsAddress(0U));
@@ -967,7 +986,7 @@ public:
 
         memory.at(6) = 0x99;
         SearchResults results2;
-        results2.Initialize(results, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
         Assert::IsFalse(results2.ContainsAddress(0U));
@@ -991,7 +1010,7 @@ public:
 
         memory.at(2) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -1023,7 +1042,32 @@ public:
 
         memory.at(4) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
+
+        Assert::AreEqual({ 1U }, results.MatchingAddressCount());
+        Assert::IsFalse(results.ContainsAddress(0U));
+        Assert::IsTrue(results.ContainsAddress(1U));
+
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(1U, result.nAddress);
+        Assert::AreEqual(MemSize::ThirtyTwoBitBigEndian, result.nSize);
+        Assert::AreEqual(0x1234AB55U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsThirtyTwoBitBigEndianLessThanPrevious)
+    {
+        std::array<unsigned char, 5> memory{ 0x00, 0x12, 0x34, 0xAB, 0x56 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 5U, ra::services::SearchType::ThirtyTwoBitBigEndian);
+        Assert::AreEqual({ 2U }, results1.MatchingAddressCount());
+
+        memory.at(4) = 0x55;
+        SearchResults results;
+        results.Initialize(results1, ComparisonType::LessThan, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 1U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -1049,7 +1093,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 3U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -1086,7 +1130,7 @@ public:
         Assert::AreEqual({ 6U }, results.MatchingAddressCount());
 
         SearchResults filtered1;
-        filtered1.Initialize(results, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 1U);
+        filtered1.Initialize(results, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"1");
         Assert::AreEqual({ 1U }, filtered1.MatchingAddressCount());
 
         SearchResults::Result result;
@@ -1098,13 +1142,13 @@ public:
         // Nibble_Upper no longer matches, but Nibble_Lower does. Neither should not be returned.
         memory.at(1) = 0x21;
         SearchResults filtered2;
-        filtered2.Initialize(filtered1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 1U);
+        filtered2.Initialize(filtered1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"1");
         Assert::AreEqual({ 0U }, filtered2.MatchingAddressCount());
 
         // Both nibbles match, only previously matched one should be returned
         memory.at(1) = 0x11;
         SearchResults filtered3;
-        filtered3.Initialize(filtered1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, 1U);
+        filtered3.Initialize(filtered1, ComparisonType::Equals, ra::services::SearchFilterType::Constant, L"1");
         Assert::AreEqual({ 1U }, filtered3.MatchingAddressCount());
 
         Assert::IsTrue(filtered3.GetMatchingAddress(0U, result));
@@ -1124,7 +1168,7 @@ public:
         Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
 
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::Constant, 0x10U);
+        results2.Initialize(results1, ComparisonType::GreaterThan, ra::services::SearchFilterType::Constant, L"0x10");
         Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
         Assert::IsFalse(results2.ContainsAddress(0U));
         Assert::IsTrue(results2.ContainsAddress(1U));
@@ -1137,7 +1181,7 @@ public:
         // Last Known Value would match indices 0, 3, and 4 as indices 1 and 2 were just changed
         // results2 limits search to indices 1 and 3, so only 3 should be matched
         SearchResults results3;
-        results3.Initialize(results1, results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results3.Initialize(results1, results2, ComparisonType::Equals, ra::services::SearchFilterType::LastKnownValue, L"");
         Assert::AreEqual({ 1U }, results3.MatchingAddressCount());
         Assert::IsFalse(results3.ContainsAddress(0U));
         Assert::IsFalse(results3.ContainsAddress(1U));
@@ -1169,7 +1213,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
         SearchResults results;
-        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
 
         Assert::AreEqual({ 2U }, results.MatchingAddressCount());
         Assert::IsFalse(results.ContainsAddress(0U));
@@ -1300,6 +1344,7 @@ public:
         Assert::IsTrue(results.ContainsAddress(35U));
         Assert::IsFalse(results.ContainsAddress(36U));
 
+        // nValue for ASCII text is the first byte of the string. use FormattedValue to get the whole string
         SearchResults::Result result;
         Assert::IsTrue(results.GetMatchingAddress(0U, result));
         Assert::AreEqual(0U, result.nAddress);
@@ -1312,6 +1357,70 @@ public:
         Assert::AreEqual({ '.' }, result.nValue);
     }
 
+    TEST_METHOD(TestInitializeFromMemoryFloat)
+    {
+        std::array<unsigned char, 8> memory{ 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x46, 0x41 }; // -2.0, 12.375
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::Float);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        Assert::IsTrue(results.ContainsAddress(0U));
+        Assert::IsTrue(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsTrue(results.ContainsAddress(3U));
+        Assert::IsTrue(results.ContainsAddress(4U));
+        Assert::IsFalse(results.ContainsAddress(5U));
+        Assert::IsFalse(results.ContainsAddress(6U));
+        Assert::IsFalse(results.ContainsAddress(7U));
+
+        // nValue is an IEE-754 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(0U, result.nAddress);
+        Assert::AreEqual(MemSize::Float, result.nSize);
+        Assert::AreEqual(0xC0000000U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(4U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::Float, result.nSize);
+        Assert::AreEqual(0x41460000U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromMemoryMBF32)
+    {
+        std::array<unsigned char, 8> memory{ 0x87, 0x46, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00 }; // 99.0, -0.5
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::MBF32);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        Assert::IsTrue(results.ContainsAddress(0U));
+        Assert::IsTrue(results.ContainsAddress(1U));
+        Assert::IsTrue(results.ContainsAddress(2U));
+        Assert::IsTrue(results.ContainsAddress(3U));
+        Assert::IsTrue(results.ContainsAddress(4U));
+        Assert::IsFalse(results.ContainsAddress(5U));
+        Assert::IsFalse(results.ContainsAddress(6U));
+        Assert::IsFalse(results.ContainsAddress(7U));
+
+        // nValue is an MBF32 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results.GetMatchingAddress(0U, result));
+        Assert::AreEqual(0U, result.nAddress);
+        Assert::AreEqual(MemSize::MBF32, result.nSize);
+        Assert::AreEqual(0x00004687U, result.nValue);
+
+        Assert::IsTrue(results.GetMatchingAddress(4U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::MBF32, result.nSize);
+        Assert::AreEqual(0x00008080U, result.nValue);
+    }
+
     TEST_METHOD(TestInitializeFromResultsAsciiText)
     {
         std::array<unsigned char, 36> memory{
@@ -1459,6 +1568,99 @@ public:
         Assert::AreEqual(30U, result.nAddress); // shore
     }
 
+    TEST_METHOD(TestInitializeFromResultsFloatEqualsConstant)
+    {
+        std::array<unsigned char, 8> memory{ 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x46, 0x41 }; // -2.0, 12.375
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::Float);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        SearchResults results2;
+        results2.Initialize(results, ComparisonType::Equals, SearchFilterType::Constant, L"-2");
+        Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
+
+        // nValue is an IEE-754 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
+        Assert::AreEqual(0U, result.nAddress);
+        Assert::AreEqual(MemSize::Float, result.nSize);
+        Assert::AreEqual(0xC0000000U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsFloatGreaterThanLast)
+    {
+        std::array<unsigned char, 8> memory{ 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x46, 0x41 }; // -2.0, 12.375
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::Float);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        memory.at(3) = 0xBF; // -2.0 -> -1.0
+        memory.at(2) = 0x80;
+
+        SearchResults results2;
+        results2.Initialize(results, ComparisonType::GreaterThan, SearchFilterType::LastKnownValue, L"");
+        Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
+
+        // nValue is an IEE-754 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
+        Assert::AreEqual(0U, result.nAddress);
+        Assert::AreEqual(MemSize::Float, result.nSize);
+        Assert::AreEqual(0xBF800000U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsMBF32EqualsConstant)
+    {
+        std::array<unsigned char, 8> memory{ 0x87, 0x46, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00 }; // 99.0, -0.5
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::MBF32);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        SearchResults results2;
+        results2.Initialize(results, ComparisonType::Equals, SearchFilterType::Constant, L"-0.5");
+        Assert::AreEqual({ 1U }, results2.MatchingAddressCount());
+
+        // nValue is an MBF32 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(0U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::MBF32, result.nSize);
+        Assert::AreEqual(0x00008080U, result.nValue);
+    }
+
+    TEST_METHOD(TestInitializeFromResultsMBF32GreaterThanLast)
+    {
+        std::array<unsigned char, 8> memory{ 0x87, 0x46, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00 }; // 99.0, -0.5
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results;
+        results.Initialize(0U, memory.size(), ra::services::SearchType::MBF32);
+        Assert::AreEqual({ 5U }, results.MatchingAddressCount());
+
+        memory.at(4) = 0x00; // -0.5 -> -0.0
+
+        SearchResults results2;
+        results2.Initialize(results, ComparisonType::GreaterThan, SearchFilterType::LastKnownValue, L"");
+        Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
+
+        // nValue is an MBF32 encoded float
+        SearchResults::Result result;
+        Assert::IsTrue(results2.GetMatchingAddress(1U, result));
+        Assert::AreEqual(4U, result.nAddress);
+        Assert::AreEqual(MemSize::MBF32, result.nSize);
+        Assert::AreEqual(0x00008000U, result.nValue);
+    }
+
     TEST_METHOD(TestCopyConstructor)
     {
         std::array<unsigned char, 5> memory{0x00, 0x12, 0x34, 0xAB, 0x56};
@@ -1476,7 +1678,7 @@ public:
         memory.at(1) = 0x14;
         memory.at(2) = 0x55;
         SearchResults results2;
-        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, 0U);
+        results2.Initialize(results1, ComparisonType::NotEqualTo, ra::services::SearchFilterType::LastKnownValue, L"");
         Assert::AreEqual({ 2U }, results2.MatchingAddressCount());
 
         SearchResults results3(results2);
@@ -1553,6 +1755,146 @@ public:
         Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(4U, MemSize::Nibble_Lower));
         Assert::AreEqual(std::wstring(L""), results1.GetFormattedValue(4U, MemSize::Nibble_Upper));
     }
+
+    TEST_METHOD(TestGetFormattedValueFloat)
+    {
+        std::array<unsigned char, 8> memory{ 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x46, 0x41 }; // -2.0, 12.375
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 8U, ra::services::SearchType::Float);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L"-2.0"), results1.GetFormattedValue(0U, MemSize::Float));
+        Assert::AreEqual(std::wstring(L"12.375"), results1.GetFormattedValue(4U, MemSize::Float));
+        Assert::AreEqual(std::wstring(L"5.91191e-39"), results1.GetFormattedValue(2U, MemSize::Float));
+        Assert::AreEqual(std::wstring(L"8192.1875"), results1.GetFormattedValue(3U, MemSize::Float));
+    }
+
+    TEST_METHOD(TestGetFormattedValueMBF32)
+    {
+        std::array<unsigned char, 8> memory{ 0x87, 0x46, 0x00, 0x00, 0x80, 0x80, 0x00, 0x00 }; // 99.0, -0.5
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1;
+        results1.Initialize(0U, 8U, ra::services::SearchType::MBF32);
+        Assert::AreEqual({ 5U }, results1.MatchingAddressCount());
+
+        Assert::AreEqual(std::wstring(L"99.0"), results1.GetFormattedValue(0U, MemSize::MBF32));
+        Assert::AreEqual(std::wstring(L"-0.5"), results1.GetFormattedValue(4U, MemSize::MBF32));
+        Assert::AreEqual(std::wstring(L"1.47513e-39"), results1.GetFormattedValue(2U, MemSize::MBF32));
+        Assert::AreEqual(std::wstring(L"1.73475e-18"), results1.GetFormattedValue(1U, MemSize::MBF32));
+    }
+
+    TEST_METHOD(TestMatchesFilterEightBitGreaterThanLast)
+    {
+        std::array<unsigned char, 5> memory{ 6, 12, 18, 24, 30 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1, results2;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+
+        SearchResults::Result result1, result2, result3;
+        results1.GetMatchingAddress(0, result1);
+        results1.GetMatchingAddress(1, result2);
+        results1.GetMatchingAddress(2, result3);
+
+        results2.Initialize(results1, ComparisonType::GreaterThanOrEqual, SearchFilterType::LastKnownValue, L"");
+
+        Assert::IsTrue(results2.MatchesFilter(results1, result1));  // 12 >= 12 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result2));  // 18 >= 18 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result3));  // 24 >= 24 ?
+
+        result1.nValue = 15;
+        result2.nValue = 15;
+        result3.nValue = 30;
+
+        Assert::IsTrue(results2.MatchesFilter(results1, result1));  // 15 >= 12 ?
+        Assert::IsFalse(results2.MatchesFilter(results1, result2)); // 15 >= 18 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result3));  // 30 >= 24 ?
+    }
+
+    TEST_METHOD(TestMatchesFilterEightBitGreaterThanLastPlus)
+    {
+        std::array<unsigned char, 5> memory{ 6, 12, 18, 24, 30 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1, results2;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+
+        SearchResults::Result result1, result2, result3;
+        results1.GetMatchingAddress(0, result1);
+        results1.GetMatchingAddress(1, result2);
+        results1.GetMatchingAddress(2, result3);
+
+        results2.Initialize(results1, ComparisonType::GreaterThanOrEqual, SearchFilterType::LastKnownValuePlus, L"4");
+
+        Assert::IsFalse(results2.MatchesFilter(results1, result1)); // 12 >= 12 + 4 ?
+        Assert::IsFalse(results2.MatchesFilter(results1, result2)); // 18 >= 18 + 4 ?
+        Assert::IsFalse(results2.MatchesFilter(results1, result3)); // 24 >= 24 + 4 ?
+
+        result1.nValue = 15;
+        result2.nValue = 22;
+        result3.nValue = 30;
+
+        Assert::IsFalse(results2.MatchesFilter(results1, result1)); // 15 >= 12 + 4 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result2));  // 22 >= 18 + 4 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result3));  // 30 >= 24 + 4 ?
+    }
+
+    TEST_METHOD(TestMatchesFilterEightBitGreaterThanLastMinus)
+    {
+        std::array<unsigned char, 5> memory{ 6, 12, 18, 24, 30 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1, results2;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+
+        SearchResults::Result result1, result2, result3;
+        results1.GetMatchingAddress(0, result1);
+        results1.GetMatchingAddress(1, result2);
+        results1.GetMatchingAddress(2, result3);
+
+        results2.Initialize(results1, ComparisonType::GreaterThanOrEqual, SearchFilterType::LastKnownValueMinus, L"4");
+
+        Assert::IsTrue(results2.MatchesFilter(results1, result1));  // 12 >= 12 - 4 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result2));  // 18 >= 18 - 4 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result3));  // 24 >= 24 - 4 ?
+
+        result1.nValue = 10;
+        result2.nValue = 14;
+        result3.nValue = 18;
+
+        Assert::IsTrue(results2.MatchesFilter(results1, result1));  // 10 >= 12 - 4 ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result2));  // 14 >= 18 - 4 ?
+        Assert::IsFalse(results2.MatchesFilter(results1, result3)); // 18 >= 24 - 4 ?
+    }
+
+    TEST_METHOD(TestMatchesFilterEightBitGreaterThanConstant)
+    {
+        std::array<unsigned char, 5> memory{ 6, 12, 18, 24, 30 };
+        ra::data::context::mocks::MockEmulatorContext mockEmulatorContext;
+        mockEmulatorContext.MockMemory(memory);
+
+        SearchResults results1, results2;
+        results1.Initialize(1U, 3U, ra::services::SearchType::EightBit);
+
+        SearchResults::Result result1, result2, result3;
+        results1.GetMatchingAddress(0, result1);
+        results1.GetMatchingAddress(1, result2);
+        results1.GetMatchingAddress(2, result3);
+
+        results2.Initialize(results1, ComparisonType::GreaterThanOrEqual, SearchFilterType::Constant, L"18");
+
+        Assert::IsFalse(results2.MatchesFilter(results1, result1)); // 12 >= 18 (12) ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result2));  // 18 >= 18 (18) ?
+        Assert::IsTrue(results2.MatchesFilter(results1, result3));  // 24 >= 18 (24) ?
+    }
 };
 
 } // namespace tests
diff --git a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
index f7cbe6fc..f0022263 100644
--- a/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/AssetListViewModel_Tests.cpp
@@ -4,6 +4,7 @@
 #include "data\models\LocalBadgesModel.hh"
 
 #include "ui\viewmodels\AssetListViewModel.hh"
+#include "ui\viewmodels\NewAssetViewModel.hh"
 
 #include "services\impl\StringTextWriter.hh"
 
@@ -2991,6 +2992,236 @@ public:
         Assert::AreEqual(0, pAsset->GetPoints());
     }
 
+    TEST_METHOD(TestCreateNewAchievementFilterTypeAchievement)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockUserContext.Initialize("User1", "FOO");
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddLeaderboard(ra::data::models::AssetCategory::Core, L"Leaderboard1");
+        vmAssetList.SetAssetTypeFilter(ra::data::models::AssetType::Achievement);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        Assert::IsTrue(vmAssetList.CanCreate());
+        vmAssetList.CreateNew();
+
+        // new Local achievement should be created and focused
+        Assert::AreEqual({ 3U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetListViewModel::FilterCategory::Local, vmAssetList.GetFilterCategory());
+        Assert::AreEqual(ra::data::models::AssetType::Achievement, vmAssetList.GetAssetTypeFilter());
+
+        const auto* pAsset = vmAssetList.FilteredAssets().GetItemAt(0);
+        Expects(pAsset != nullptr);
+        Assert::IsTrue(pAsset->IsSelected());
+        Assert::AreEqual(std::wstring(), pAsset->GetLabel());
+        Assert::AreEqual(AssetCategory::Local, pAsset->GetCategory());
+        Assert::AreEqual(AssetState::Inactive, pAsset->GetState());
+        Assert::AreEqual(AssetChanges::New, pAsset->GetChanges());
+        Assert::AreEqual({ 111000001U }, pAsset->GetId());
+        Assert::AreEqual(0, pAsset->GetPoints());
+
+        // and loaded in the editor, which should be shown (local achievement will always have ID 0)
+        Assert::AreEqual({ 0U }, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAssetLoaded());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAchievement());
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsLeaderboard());
+        Assert::IsTrue(bEditorShown);
+    }
+
+    TEST_METHOD(TestCreateNewLeaderboardFilterTypeLeaderboard)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockUserContext.Initialize("User1", "FOO");
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddLeaderboard(ra::data::models::AssetCategory::Core, L"Leaderboard1");
+        vmAssetList.SetAssetTypeFilter(ra::data::models::AssetType::Leaderboard);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        Assert::IsTrue(vmAssetList.CanCreate());
+        vmAssetList.CreateNew();
+
+        // new Local leaderboard should be created and focused
+        Assert::AreEqual({ 3U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetListViewModel::FilterCategory::Local, vmAssetList.GetFilterCategory());
+        Assert::AreEqual(ra::data::models::AssetType::Leaderboard, vmAssetList.GetAssetTypeFilter());
+
+        const auto* pAsset = vmAssetList.FilteredAssets().GetItemAt(0);
+        Expects(pAsset != nullptr);
+        Assert::IsTrue(pAsset->IsSelected());
+        Assert::AreEqual(std::wstring(), pAsset->GetLabel());
+        Assert::AreEqual(AssetCategory::Local, pAsset->GetCategory());
+        Assert::AreEqual(AssetState::Inactive, pAsset->GetState());
+        Assert::AreEqual(AssetChanges::New, pAsset->GetChanges());
+        Assert::AreEqual({ 111000001U }, pAsset->GetId());
+        Assert::AreEqual(0, pAsset->GetPoints());
+
+        // and loaded in the editor, which should be shown (local achievement will always have ID 0)
+        Assert::AreEqual({ 0U }, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAssetLoaded());
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsAchievement());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsLeaderboard());
+        Assert::IsTrue(bEditorShown);
+    }
+
+    TEST_METHOD(TestCreateNewAchievementFilterTypeAll)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockUserContext.Initialize("User1", "FOO");
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddLeaderboard(ra::data::models::AssetCategory::Core, L"Leaderboard1");
+        vmAssetList.SetAssetTypeFilter(ra::data::models::AssetType::None);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+
+        bool bPromptShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<NewAssetViewModel>([&bPromptShown](NewAssetViewModel& vmNewAsset)
+        {
+            bPromptShown = true;
+            vmNewAsset.NewAchievement();
+            return DialogResult::OK;
+        });
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        Assert::IsTrue(vmAssetList.CanCreate());
+        vmAssetList.CreateNew();
+
+        Assert::IsTrue(bPromptShown);
+
+        // new Local achievement should be created and focused
+        Assert::AreEqual({ 3U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetListViewModel::FilterCategory::Local, vmAssetList.GetFilterCategory());
+        Assert::AreEqual(ra::data::models::AssetType::None, vmAssetList.GetAssetTypeFilter());
+
+        // and loaded in the editor, which should be shown (local achievement will always have ID 0)
+        Assert::AreEqual({ 0U }, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAssetLoaded());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAchievement());
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsLeaderboard());
+        Assert::IsTrue(bEditorShown);
+    }
+
+    TEST_METHOD(TestCreateNewLeaderboardFilterTypeAll)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockUserContext.Initialize("User1", "FOO");
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddLeaderboard(ra::data::models::AssetCategory::Core, L"Leaderboard1");
+        vmAssetList.SetAssetTypeFilter(ra::data::models::AssetType::None);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+
+        bool bPromptShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<NewAssetViewModel>([&bPromptShown](NewAssetViewModel& vmNewAsset)
+        {
+            bPromptShown = true;
+            vmNewAsset.NewLeaderboard();
+            return DialogResult::OK;
+        });
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        Assert::IsTrue(vmAssetList.CanCreate());
+        vmAssetList.CreateNew();
+
+        Assert::IsTrue(bPromptShown);
+
+        // new Local leaderboard should be created and focused
+        Assert::AreEqual({ 3U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 1U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetListViewModel::FilterCategory::Local, vmAssetList.GetFilterCategory());
+        Assert::AreEqual(ra::data::models::AssetType::None, vmAssetList.GetAssetTypeFilter());
+
+        // and loaded in the editor, which should be shown (local achievement will always have ID 0)
+        Assert::AreEqual({ 0U }, vmAssetList.mockWindowManager.AssetEditor.GetID());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsAssetLoaded());
+        Assert::IsFalse(vmAssetList.mockWindowManager.AssetEditor.IsAchievement());
+        Assert::IsTrue(vmAssetList.mockWindowManager.AssetEditor.IsLeaderboard());
+        Assert::IsTrue(bEditorShown);
+    }
+
+    TEST_METHOD(TestCreateNewCancelFilterTypeAll)
+    {
+        AssetListViewModelHarness vmAssetList;
+        vmAssetList.mockUserContext.Initialize("User1", "FOO");
+        vmAssetList.MockGameId(22U);
+        vmAssetList.AddAchievement(AssetCategory::Core, 5, L"Test1", L"Desc1", L"12345", "0xH1234=1");
+        vmAssetList.AddLeaderboard(ra::data::models::AssetCategory::Core, L"Leaderboard1");
+        vmAssetList.SetAssetTypeFilter(ra::data::models::AssetType::None);
+        vmAssetList.ForceUpdateButtons();
+
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+
+        bool bPromptShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<NewAssetViewModel>([&bPromptShown](NewAssetViewModel&)
+        {
+            bPromptShown = true;
+            return DialogResult::Cancel;
+        });
+
+        bool bEditorShown = false;
+        vmAssetList.mockDesktop.ExpectWindow<AssetEditorViewModel>([&bEditorShown](AssetEditorViewModel&)
+        {
+            bEditorShown = true;
+            return DialogResult::None;
+        });
+
+        Assert::IsTrue(vmAssetList.CanCreate());
+        vmAssetList.CreateNew();
+
+        Assert::IsTrue(bPromptShown);
+
+        // no new item should have been created. existing filters should be maintained
+        Assert::AreEqual({ 2U }, vmAssetList.mockGameContext.Assets().Count());
+        Assert::AreEqual({ 2U }, vmAssetList.FilteredAssets().Count());
+        Assert::AreEqual(AssetListViewModel::FilterCategory::Core, vmAssetList.GetFilterCategory());
+        Assert::AreEqual(ra::data::models::AssetType::None, vmAssetList.GetAssetTypeFilter());
+
+        // editor should not be opened
+        Assert::IsFalse(bEditorShown);
+    }
+
     TEST_METHOD(TestCloneSingle)
     {
         AssetListViewModelHarness vmAssetList;
diff --git a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
index 4a2ec6d8..67f5a845 100644
--- a/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
+++ b/tests/ui/viewmodels/MemorySearchViewModel_Tests.cpp
@@ -151,7 +151,7 @@ public:
         Assert::AreEqual(std::wstring(L""), search.GetFilterRange());
         Assert::AreEqual(std::wstring(L""), search.GetFilterValue());
 
-        Assert::AreEqual({ 9U }, search.SearchTypes().Count());
+        Assert::AreEqual({ 11U }, search.SearchTypes().Count());
         Assert::AreEqual((int)ra::services::SearchType::FourBit, search.SearchTypes().GetItemAt(0)->GetId());
         Assert::AreEqual(std::wstring(L"4-bit"), search.SearchTypes().GetItemAt(0)->GetLabel());
         Assert::AreEqual((int)ra::services::SearchType::EightBit, search.SearchTypes().GetItemAt(1)->GetId());
@@ -168,8 +168,12 @@ public:
         Assert::AreEqual(std::wstring(L"16-bit BE"), search.SearchTypes().GetItemAt(6)->GetLabel());
         Assert::AreEqual((int)ra::services::SearchType::ThirtyTwoBitBigEndian, search.SearchTypes().GetItemAt(7)->GetId());
         Assert::AreEqual(std::wstring(L"32-bit BE"), search.SearchTypes().GetItemAt(7)->GetLabel());
-        Assert::AreEqual((int)ra::services::SearchType::AsciiText, search.SearchTypes().GetItemAt(8)->GetId());
-        Assert::AreEqual(std::wstring(L"ASCII Text"), search.SearchTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::Float, search.SearchTypes().GetItemAt(8)->GetId());
+        Assert::AreEqual(std::wstring(L"Float"), search.SearchTypes().GetItemAt(8)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::MBF32, search.SearchTypes().GetItemAt(9)->GetId());
+        Assert::AreEqual(std::wstring(L"MBF32"), search.SearchTypes().GetItemAt(9)->GetLabel());
+        Assert::AreEqual((int)ra::services::SearchType::AsciiText, search.SearchTypes().GetItemAt(10)->GetId());
+        Assert::AreEqual(std::wstring(L"ASCII Text"), search.SearchTypes().GetItemAt(10)->GetLabel());
         Assert::AreEqual(ra::services::SearchType::EightBit, search.GetSearchType());
 
         Assert::AreEqual({ 6U }, search.ComparisonTypes().Count());